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



我将把它简化为尽可能简单的案例,但这发生在所有事情上。

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

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 关键字或其他东西(,否则我看不出任何理由为什么这会根据我在下拉列表中所做的选择将新类别作为现有类别的副本添加到数据库中。

为什么会这样?我在这里做错了什么?

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 上下文(。

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

但是,您分配给clientCategory似乎没有 PK 集,并且(很可能(只有名称集,并且您没有唯一的 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
}

最新更新