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

Posted

技术标签:

【中文标题】在分离场景中使用独立关联时更新一对多实体【英文标题】:Update One-to-Many entities when using independent associations in a detached scenario 【发布时间】:2014-07-20 13:47:50 【问题描述】:

我是使用 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));
    

批处理和文档之间存在一对多关系,通过独立关联建模,因此没有明确的 FK。 一个重要的特殊性是 Document 类不知道 Batch。 这与 *** 上的其他类似问题不同,例如 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:附加类型的实体 “文档”失败,因为另一个相同类型的实体已经拥有 相同的主键值。使用“附加”时可能会发生这种情况 方法或将实体的状态设置为“未更改”或“已修改” 如果图中的任何实体具有冲突的键值。这可能是 因为有些实体是新的,还没有收到 数据库生成的键值。在这种情况下,使用“添加”方法或 'Added' 实体状态来跟踪图形,然后设置状态 将非新实体酌情改为“未更改”或“已修改”。

应该如何实现 Update() 方法才能正确执行更新?

谢谢你,如果这个问题听起来令人困惑,我很抱歉,这完全是由于我对 EF 的困惑。

【问题讨论】:

【参考方案1】:

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

问题在于,当断开连接的实体是断开连接树的根时,您必须照顾相关实体。有多种可能的方法:

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

请注意,这种行为是可取的。例如,如果您有多对多关系,则新的孩子可能是全新的(添加的)或预先存在的(未更改的)。或者,您可以有一个一对多关系的父实体,其中包含已删除的子实体(已删除)和新实体(已添加)。因此,您始终必须跟踪一组每个实体的状态。

【讨论】:

感谢您的回复。我会将其标记为我的问题的已接受答案,因为它指出了我需要自己照顾实体的事实。我将在我的示例中包含一个带有特定解决方案的附加答案,也许其他人会从中受益。【参考方案2】:

按照@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();
            
        
    

【讨论】:

以上是关于在分离场景中使用独立关联时更新一对多实体的主要内容,如果未能解决你的问题,请参考以下文章

4.一对多关联映射

MyBatis关联映射:一对一一对多总结一二

mybatis关联查询,一对一,一对多

JPA中实现单向一对多的关联关系

hibernate一对多单向关联时更新问题

JPA一对多映射