在分离场景中使用独立关联时更新一对多实体
本文关键字:关联 更新 一对多 实体 独立 分离 | 更新日期: 2023-09-27 18:06:10
我是使用实体框架6的新手,我被困在试图更新一个实体。我正在使用代码优先的方法,我的模型,减少到问题类,看起来像这样:
public class Document
{
public long Key { get; set; }
public string Name { get; set; }
}
public class Batch
{
public long Key { get; set; }
public virtual List<Document> Documents { get; private set; }
public void Add(Document document)
{
Documents.Add(document);
}
public void Remove(Document document)
{
Documents.RemoveAt(Documents.FindIndex(d => d.Key == document.Key));
}
}
Batch和Document之间存在一对多的关系,通过独立的关联建模,因此没有显式的FK。一个重要的特殊性是Document类不知道Batch。这与Stackoverflow上的其他类似问题不同,例如这个问题。实体框架在文档表中生成一个可空列Batch_Key。
这是上下文类:
public class MyDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>().HasKey(d => d.Key);
modelBuilder.Entity<Batch>().HasKey(b => b.Key);
modelBuilder.Entity<Batch>().HasMany(b => b.Documents).WithOptional().WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
}
我有一个带有Update()方法的BatchRepository类,该方法接收批处理参数并更新数据库中的批处理:
public class BatchRepository
{
public void Update(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
batchesContext.Batches.Attach(item);
batchesContext.Entry(item).State = EntityState.Modified;
batchesContext.SaveChanges();
}
}
}
下面是一个单元测试,用于从批处理中删除文档,它使用Update()方法:
[TestMethod]
public void CheckRemoveDocument()
{
var batches = BatchRepository.FindAll().ToList();
var batch = batches[0];
var batchKey = batches[0].Key;
var doc = batch.Documents[0];
int batchNumberOfDocuments = batch.Documents.Count;
batch.Remove(doc);
BatchRepository.Update(batch);
batch = BatchRepository.FindBy(batchKey);
Assert.AreEqual(batchNumberOfDocuments - 1, batch.Documents.Count);
}
测试失败,batchNumberOfDocuments与之前相同。如果我实现一个方法从批处理中删除所有文档,像这样:
public class BatchRepository
{
public void RemoveDocuments(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
var existingBatch = batchesContext.Batches.Find(item.Key);
batchesContext.Entry(existingBatch).Collection(b => b.Documents).Load();
existingBatch.Documents.RemoveAll(d => true);
batchesContext.SaveChanges();
}
}
}
下面的测试成功:
[TestMethod]
public void CheckRemoveAllDocuments()
{
var batches = BatchRepository.FindAll().ToList();
var batch = batches[0];
var batchKey = batches[0].Key;
BatchRepository.RemoveDocuments(batch);
batch = BatchRepository.FindBy(batchKey);
Assert.AreEqual(0, batch.Documents.Count);
}
因此EF正确地跟踪批处理和文档之间的关系,将删除的文档的Batch_Key列设置为NULL。为什么这在这种情况下工作,而不是在我更新的地方?我认为这是因为链接到批处理的Document实体没有附加到上下文。问题是,我不知道Update()方法参数的批处理是否有更多的文档,更少的文档,或者是一个完全不同的文档列表(也可能是Document列表之外的其他东西发生了变化)。
这样的实现:
public class BatchRepository
{
public void Update(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
var existingBatch = batchesContext.Batches.Find(item.Key);
batchesContext.Entry(existingBatch).Collection(b => b.Documents).Load();
foreach (var doc in item.Documents)
{
batchesContext.Documents.Attach(doc);
batchesContext.Entry(doc).State = EntityState.Modified;
}
batchesContext.SaveChanges();
}
}
}
抛出异常:
系统。InvalidOperationException:附加一个类型的实体"文档"失败,因为另一个相同类型的实体已经存在相同的主键值。使用"Attach"时可能会发生这种情况方法或将实体的状态设置为"未更改"或"修改"。如果图中的任何实体具有冲突的键值。这可能是因为有些实体是新的,还没有收到数据库生成的键值。在本例中,使用'Add'方法或"添加"的实体状态来跟踪图形,然后设置状态非新实体视情况更改为"未更改"或"修改"。
如何实现Update()方法来正确执行更新?
谢谢你,如果问题听起来令人困惑,这都是由于我脑海中与EF有关的混乱。
很明显,当您从上下文断开一个实体时,它的状态不会被跟踪。因此,当你这样做时,你需要将实体附加回上下文并设置正确的状态。
问题是,当不连接的实体是不连接树的根时,你必须照顾相关的实体。有两种可能的方法:
- 使用
Attach()
:在这种情况下,您需要单独附加每个实体,并设置每个实体的状态。 - 使用
Add()
:在这种情况下,整个树一次附加,所有实体都获得Added
状态。所以你需要设置每个实体的状态,除了新的实体(因为它们已经有Added
状态)。
注意,这种行为是可取的。例如,如果您有多对多关系,则新的子节点可以是全新的(添加的),也可以是预先存在的(未更改的)。或者,您可以有一个一对多关系的父实体,它具有已删除的子实体(已删除)和新实体(已添加)。因此,您必须始终跟踪和设置每个实体的状态。
按照@JotaBe的建议,我自己跟踪所有的Document实体。下面是代码:
public class BatchRepository
{
protected override void Update(Batch item)
{
using (var dbCtx = new MyDbContext(DBContextName))
{
var existingBatch = dbCtx.Batches.Find(item.Key);
if (null != existingBatch)
{
// Load the documents for the existing batch
dbCtx.Entry(existingBatch).Collection(b => b.Documents).Load();
// Get the list of the documents that were removed from the existing batch
var removedDocuments = existingBatch.Documents.Except(item.Documents).ToList();
foreach (var doc in removedDocuments)
{
// Remove the relationship between the documents and the batch
existingBatch.Documents.Remove(doc);
}
// Get the list of the newly added documents
var addedDocuments = item.Documents.Except(existingBatch.Documents).ToList();
foreach (var doc in addedDocuments)
{
// The document exists in the repository, so we just attach it to the context
dbCtx.Documents.Attach(doc);
// Create the relation between the batch and document
existingBatch.Documents.Add(doc);
}
// Overwrite all property current values from modified batch' entity values,
// so that it will have all modified values and mark entity as modified.
var batchEntry = dbCtx.Entry(existingBatch);
batchEntry.CurrentValues.SetValues(item);
dbCtx.SaveChanges();
}
}
}
}