为什么 EF 为我未指定的实体插入新数据?

Why is EF inserting new data for entities that I'm not specifying?

我将尽可能将其分解为一个简单的案例,但这种情况适用于所有情况。

我的大部分数据模型 POCO 对象都基于如下定义的 BaseDataObject:

public class BaseDataObject
{
    public int Id { get; set; }
    public bool Deleted { get; set; }
}

我的代码优先数据模型有一个 Client 对象:

public class Client : BaseDataObject
{
    public string Name { get; set; }
    public virtual Category Category { get; set; }
    public virtual Category Subcategory { get; set; }
}

Category 对象非常简单:

public class Category : BaseDataObject
{
    public string Name { get; set; }
}

继承的BaseDataObject中存在所需的Id属性。

要添加实体,我使用以下存储库:

public class DataRepository<TModel, TContext>
    where TModel : BaseDataObject
    where TContext : DbContext
{
    public int AddItem(T item)
    {
        using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
        {
            db.Set<T>().Add(item);
            db.SaveChanges();
        }
    }

    // These are important as well.
    public List<T> ListItems(int pageNumber = 0)
    {
        using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
        {
            // Deleted property is also included in BaseDataObject.
            return db.Set<T>().Where(x => !x.Deleted).OrderBy(x => x.Id).Skip(10 * pageNumber).ToList();
    }

    public T GetSingleItem(int id)
    {
        using (var db = (TContext)Activator.CreateInstance(typeof(TContext)))
        {
            return db.Set<T>().SingleOrDefault(x => x.Id == id && !x.Deleted);
        }
    }
}

这添加了一个新的客户端非常好,但是这里我的数据模型有一些奇怪的地方导致 Entity Framework 每次我添加一个基于我选择的类别的客户端时也会添加 2 个新的类别我的表格。

这是我的表单代码:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        try
        {
            BindDropDownList<Category>(CategoryList);
            BindDropDownList<Category>(SubcategoryList);
        }
        // Error handling things
    }
}

private void BindDropDownList<TModel>(DropDownList control) where TModel : BaseDataObject
{
    var repo = new DataRepository<TModel, ApplicationDbContext>();
    control.DataSource = repo.ListItems();
    control.DataTextField = "Name";
    control.DataValueField = "Id";
    control.DataBind();
    control.Items.Insert(0, new ListItem("-- Please select --", "0"));
}
private TModel GetDropDownListSelection<TModel>(DropDownList control) where TModel : BaseDataObject
{
    var repo = new DataRepository<TModel, ApplicationDbContext>();
    int.TryParse(control.SelectedItem.Value, out int selectedItemId);
    return repo.GetSingleItem(selectedItemId);
}

protected void SaveButton_Click(object sender, EventArgs e)
{
    try
    {
        var repo = new DataRepository<Client, ApplicationDbContext();

        var selectedCategory = GetDropDownListSelection<Category>(CategoryList);
        var selectedSubcategory = GetDropDownListSelection<Category>(SubcategoryList);
        var name = NameTextBox.Text;

        var client = new Client
        {
            Name = name,
            Category = selectedCategory,
            Subcategory = selectedSubcategory
        };

        repo.AddItem(client);
    }
    // Error handling things
}

除非我在这里创建关系的方式有问题(可能使用 virtual 关键字或其他东西),否则我看不出有任何理由将新类别添加到数据库中作为现有类别的副本基于我在下拉列表中所做的选择。

为什么会这样?我哪里错了?

很难从您的列表中判断,因为没有包含 FK 映射,也没有提供基本模型详细信息。

但是,您分配给 clientCategory 似乎没有 PK 设置,并且(很可能)只有 Name 设置,并且您没有唯一的 IX那。

所以 EF 没有合理的方法来确定这是正确的类别。

一种排序方法是

protected void SaveButton_Click(object sender, EventArgs e)
{
    try
    {
        var repo = new DataRepository<Client, ApplicationDbContext>();

        var selectedCategory = GetDropDownListSelection<Category>(CategoryList);
        var selectedSubcategory = GetDropDownListSelection<Category>(SubcategoryList);
        var name = NameTextBox.Text;

        var client = new Client
        {
            Name = name,
            // either
            Category = new DataRepository<Category , ApplicationDbContext>().GetSingleItem(selectedCategory.id),
            // or, easier (assuming you have FK properties defined on the model)
            CategoryId = selectedCategory.Id,
            // repeat as needed
            Subcategory = selectedSubcategory
        };

        repo.AddItem(client);
    }
    // Error handling things
}

DbSet<T>.Add 方法递归地级联到当前未被上下文跟踪的导航属性,并将它们标记为 Added。所以当你这样做时

db.Set<T>().Add(item);

它实际上将 Client class 引用的 Category 实体都标记为 Added,因此 SaveChanges 插入了两个新的重复 Category 记录。

通常的解决方案是通过提前将实体附加到上下文来告诉 EF 实体存在。例如,如果您将 repo.AddItem(client); 替换为

using (var db = new ApplicationDbContext())
{
    if (client.Category != null) db.Set<Category>().Attach(client.Category);
    if (client.Subcategory != null) db.Set<Category>().Attach(client.Subcategory);
    db.Set<Client>().Add(item);
    db.SaveChanges();    
}

一切都会好起来的。

问题是您使用的通用存储库实现没有为您提供必要的控制。但那是你的设计决策问题,而不是 EF。以上是 EF 处理此类操作的预期方式。如何将它融入您的设计取决于您(我个人会消除通用存储库反模式并直接使用数据库上下文)。