在分离场景中使用独立关联时更新一对多实体

本文关键字:关联 更新 一对多 实体 独立 分离 | 更新日期: 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();
            }
        }
    }
}