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

时间:2018-08-23 11:22:31

标签: c# entity-framework-6 ef-code-first

我将尽可能简化这种情况,但这在所有情况下都会发生。

我将大部分数据模型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; }
}

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

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

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);
        }
    }
}

这添加了一个新的客户端,这很好,但是这里的数据模型有些奇怪,这导致实体框架每次根据基于表单中选择的类别添加客户端时,实体框架也会添加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关键字或其他方式),否则我看不到任何理由将这种新的类别作为基于现有类别的重复项添加到数据库中我在下拉列表中所做的选择。

为什么会这样?我这是怎么了?

2 个答案:

答案 0 :(得分:3)

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

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

它实际上将两个Client引用的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处理此类操作的预期方式。如何将其适合您的设计由您决定(我个人将消除通用存储库反模式,而直接使用db上下文)。

答案 1 :(得分:0)

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

但是,似乎分配给Category的{​​{1}}没有PK设置,并且(很可能)只有Name设置,并且在此上没有唯一的IX。

因此,EF没有合理的方法来确定这是正确的类别。

一种排序方式是

client