【问题标题】:Duplicating EF Entity on my model but avoid duplicate specific child entity element在我的模型上复制 EF 实体,但避免重复特定的子实体元素
【发布时间】:2015-02-15 13:26:43
【问题描述】:

我需要复制一个 Transport 实体(包括子实体,但其中一个除外:Documents which of type 'Mail')。

这是我的模型:

public class Transport
{
    [Key]
    public int Id { get; set; }
    public string TransportNumber { get; set; }
    ...
    public virtual List<Document> Documents { get; set; }
}

public class Document
{
    [Key]
    public int Id { get; set; }
    public EnumDocumentType Type { get; set; }
    public int FileId { get; set; }
    public virtual File File { get; set; }
    public virtual Transport Transport { get; set; }
}

public class File
{
    [Key]
    public int Id { get; set; }
    public string Filename { get; set; }
    public byte[] FileStream { get; set; }
}

我用于复制的 c# 代码:

   public void DuplicateTransports(IEnumerable<int> ids) {

        var transportsToDuplicate = Context.Transports.Where(t => ids.Contains(t.Id))
                                                      .Include(c => c.Documents).ToList();

        transportsToDuplicate.ForEach(t =>
        {
            var newTransport = Context.Transports.Add(t);
            newTransport.Id = -100;
            AttributeNumber(newTransport);

            t.Documents.ForEach(doc =>
                {
                    ObjectContext.LoadProperty(doc, x => x.File);
                    Context.Documents.Add(doc);
                    Context.Files.Add(doc.File);
                });
            Context.SaveChanges();

            // (step 2) Don't duplicate child Documents of type Mail 
            var docs = Context.Documents.Where(doc => doc.TransportId == newTransport.Id && doc.Type == EnumDocumentType.Mail).ToList(); 
            for (int i=docs.Count-1; i>=0; i--) Context.Documents.Remove(docs[i]);
            Context.SaveChanges();

        });

它按预期工作,但正如您在代码中看到的那样,我必须分两步进行:

  • 第 1 步:复制元素 + 在上下文中保存更改
  • 第 2 步:搜索 Mail 类型的文档并将其删除 + 在上下文中保存更改

我的问题:是否可以只进行 1 步?因此,请避免添加 Mail 类型的上下文文档,因为在我的情况下,我需要在下一步删除它,我发现它没有优化。

谢谢。


编辑

按照 Philip Stuyckx 的建议,我已经尝试像这样进行 1 步:

        transportsToDuplicate.ForEach(t =>
        {
            var newTransport = Context.Transports.Add(t);
            newTransport.Id = -100;
            AttributeNumber(newTransport);

            t.Documents.ForEach(doc =>
                {
                    if (doc.Type != EnumDocumentType.Mail)
                    {
                        ObjectContext.LoadProperty(doc, x => x.File);
                        Context.Documents.Add(doc);
                        Context.Files.Add(doc.File);
                    }
                });
            Context.SaveChanges();
     });

然后出现异常:对数据库的更改已成功提交,但在更新对象上下文时出错。 ObjectContext 可能处于不一致的状态。内部异常消息:发生引用完整性约束冲突:关系一端的“Transport.Id”的属性值与另一端的“Document.TransportId”的属性值不匹配。

【问题讨论】:

    标签: c# entity-framework


    【解决方案1】:

    我认为以下方法可以解决问题:

    t.Documents.Where(doc => doc.Type == EnumDocumentType.Mail).ForEach(doc =>
                {
                    ObjectContext.LoadProperty(doc, x => x.File);
                    Context.Documents.Add(doc);
                    Context.Files.Add(doc.File);
                });
    

    问题也是为什么你一直做 saveChanges 这么多次,最后一个就足够了。

    transportsToDuplicate.ForEach(t =>
       ....
    }
    Context.SaveChanges();
    

    但也许你有理由这样做?

    【讨论】:

    • 您建议的代码给出了以下错误:对数据库的更改已成功提交,但在更新对象上下文时出错。 ObjectContext 可能处于不一致的状态。内部异常消息:发生引用完整性约束冲突:关系一端的“Transport.Id”的属性值与另一端的“Document.TransportId”的属性值不匹配。这就是为什么我必须分两步进行编码,如我的问题所示。我希望有更好的方法。
    • 你真的需要那些调用 saveChanges 吗?
    • 是的,因为我在这里只向您展示了一小部分代码,并且当 SaveChanges 语句之前的操作过多时出现异常。
    【解决方案2】:

    我认为“一步”是指插入步骤。如果您设法仅获取要插入的数据,则可以。但是,您需要在一个查询中执行更多步骤或更多技巧来获取数据。我会坚持先在一个简单的查询中获取数据,然后处理它们以过滤掉您不想复制的数据。

    这看起来像您当前的流程,但我会采取稍微不同的方法。 (都假设ContextDbContext)。

    首先,您应该使用AsNoTracking() 获取传输和子数据。然后你可以Add()他们到上下文中,而不会得到实体键错误或不明确的关联(在添加之前上下文仍然是空的):

    var transportsToDuplicate = Context.Transports
                                       .AsNoTracking()
                                       .Where(t => ids.Contains(t.Id))
                                       .Include(c => c.Documents.Select(d => d.File))
                                       .ToList();
    
    transportsToDuplicate.ForEach(t => Context.Transports.Add(t));
    

    现在每个Transport所有 子对象都处于Added 状态(因为它们尚未被跟踪)并将作为新对象插入。

    如果您现在调用SaveChanges(),所有数据都将被复制。

    所以现在唯一要做的就是防止邮件文档及其文件被重复:

    var mailDocuments = Context.Documents.Local // Local!
                               .Where(d => d.Type == EnumDocumentType.Mail)
                               .ToList();
    foreach(var md in mailDocuments)
    {
        Context.Entry(md).State = System.Data.Entity.EntityState.Detached;
        if (md.File != null)
        {
            Context.Entry(md.File).State = System.Data.Entity.EntityState.Detached;
        }
    }
    

    邮件文档及其文件将被分离,不会被插入。

    现在您可以拨打SaveChanges

    您可以在此处阅读更多背景信息:Merge identical databases into one

    【讨论】:

    • 很有启发性。感谢那。我注意到它可以在 Transport 实体上不使用 AsNoTracking 而在 Documents 实体上仅使用 Local 的情况下工作。
    【解决方案3】:

    您应该避免将您的Documents/Files 与数据库中的多个Transports 相关联,否则您会遇到级联错误。如果您不小心,您将拥有一个由两个单独的Documents 引用的File,您将删除一个较旧的Transport - 例如,一个带有DocumentType.Mail 的@ - 它的Document,和DocumentFile,然后属于另一个Transport 的另一个Document 将指代那个不再存在的File

    Transport 对象应被视为aggregate root,并且与其关联的所有Documents 和Files 应被视为聚合的一部分。当Transport 对象从数据库中删除时,其关联的Documents 和Files 也应该被删除;关联的实体应被视为属于Transport 对象。

    您应该为要复制的每个 Transport 创建全新的聚合,根植于新的 Transport,以避免多次引用同一实体:

    public void DuplicateTransports(IEnumerable<int> ids)
    {
        using (var context = new MyContext())
        {
            var transportsToDuplicate = context.Transports
                                               .Include(c => c.Documents.Select(d => d.File)) // load entire aggregate record in a single query
                                               .Where(t => ids.Contains(t.Id));
    
            foreach (var transport in transportsToDuplicate)
            {
                var documents = transport.Documents
                                         .Where(d => d.Type != Document.DocumentType.Mail)
                                         .Select(d => new Document
                                         {
                                             Type = d.Type,
                                             File = new File
                                             {
                                                 Filename = d.File.Filename,
                                                 FileStream = d.File.FileStream,
                                             }
                                         });
    
                var newTransport = new Transport { TransportNumber = transport.TransportNumber };
                newTransport.Documents.AddRange(documents);
                context.Transports.Add(newTransport);
            }
    
            context.SaveChanges();
        }
    }
    

    编辑

    您可能会注意到DocumentFile 的所有细节都散布在我们的复制方法中,应该更多地关注复制而不是复制。如果我们添加复制构造函数,我们会将进入 DocumentFile 的内容的详细信息隔离到类本身中,并简化我们的重复代码,这也将使其更便于将来对这些类进行更改。

    使用以下演员:

    public File() { }
    public File(File file) : this()
    {
        this.Filename = file.Filename;
        this.FileStream = file.FileStream;
    }
    
    public Document() { }
    public Document(Document document) : this()
    {
        this.Type = document.Type;
        this.File = new File(document.File);
    }
    

    DuplicateTransports 中的 foreach 循环简化了:

    foreach (var transport in transportsToDuplicate)
    {
        var documents = transport.Documents
                                 .Where(d => d.Type != Document.DocumentType.Mail)
                                 .Select(d => new Document(d));
        var newTransport = new Transport { TransportNumber = transport.TransportNumber };
        newTransport.Documents.AddRange(documents);
        context.Transports.Add(newTransport);
    }
    

    【讨论】:

    • 您可以使用DbContext 上的EntityState 和/或访问DbSet.LocalDbSet 中先前获取的实体的缓存集合)玩一些技巧,但这些是元数据,并且每当我考虑使用元数据变得可爱时,我都会后悔,并用更直接的东西取而代之; (当然,很多时候是因为我在一个团队中工作,而团队中的某些人对 EF 元数据的了解较少,但即使团队成员只有我和一年后的我,也很简单通常是最好的)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-31
    • 1970-01-01
    • 2011-06-17
    • 1970-01-01
    • 2016-02-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多