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

时间:2014-05-30 17:57:56

标签: c# entity-framework entity-framework-6

我是使用Entity Framework 6的新手,我在尝试更新实体时遇到困难。我使用Code First方法,我的模型缩减为相关类,看起来像这样:

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上的其他类似问题有所不同,例如this one。实体框架在Document表中生成一个可以为空的列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类,它接收Batch参数并更新数据库中的批处理:

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以删除文档。为什么这在这种情况下有效,而在我正在更新的情况下呢?我认为这是因为链接到批处理的文档实体未附加到上下文。问题是我不知道Update()方法的参数中的批次是否有更多的文档,更少的文档或完全不同的文档列表(也可能是除了文件清单已更改)。

这样的实现:

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

抛出异常:

  

System.InvalidOperationException:附加类型的实体   &#39;文件&#39;失败,因为同一类型的另一个实体已经有了   相同的主键值。使用&#39;附加&#39;   方法或将实体的状态设置为“未更改”#39;或者&#39;修改&#39;   如果图中的任何实体具有冲突的键值。这可能是   因为有些实体是新的,还没有收到   数据库生成的键值。在这种情况下,请使用&#39;添加&#39;方法或   &#39;已添加&#39;实体状态跟踪图形然后设置状态   非新实体“未变”&#39;或者&#39;修改&#39;酌情。

如何实施Update()方法以正确执行更新?

谢谢你,如果这个问题听起来令人困惑,那么这一切都归功于我对EF的疑惑。

2 个答案:

答案 0 :(得分:2)

很明显,您知道当您将实体与上下文断开连接时,它的状态不会被跟踪。因此,正如您所做的那样,您需要将实体附加回上下文并设置正确的状态。

问题在于,当被删除的实体是断开连接的树的根时,您必须处理相关的实体。有可能的方法:

  • 使用Attach():在这种情况下,您需要单独附加每个实体,并设置每个实体的状态。
  • 使用Add():在这种情况下,整个树都会立即附加,并且所有实体都会获得Added状态。所以你需要设置每个实体的状态,除了新实体(因为它们已经具有Added状态)。

请注意,此行为是可取的。例如,如果你有多对多的关系,那么新的孩子可以是全新的(添加的)或者是预先存在的(不变的)。或者,您可以拥有与已删除子项(已删除)和新实体(已添加)的一对多关系的父实体。因此,您始终必须跟踪每个实体的状态。

答案 1 :(得分:2)

按照@ JotaBe的建议,我自己跟踪所有文档实体。 这是代码:

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