如何使用EF向不相关的表插入记录

问题描述

我有一个 Comment 表,它可以链接到许多具有评论的不同实体,但由于某些原因,我没有链接这些表。相反,Comment 包含 TableReferenceIdEntryReferenceIdTableReferenceId 只是一个 int,我们可以在应用层中检查该评论所指的实体/表,而 EntryReferenceId一个 int,它指的是所述实体/表中的特定条目,评论属于。

通过表格和条目引用查询这样的评论会很好,但是在插入批量数据时,我画了一个空白。例如,如果我有 Vehicle 实体并且 Vehicle 可以有很多评论,那么在插入数据时,由于我还没有 VehicleId,我将如何链接它们?这是可行的还是对链接评论的每个表走多对多路线更好?

解决方法

如果你可以避免这种情况,那么你应该尝试,或者你应该尽量避免支持批量插入。不过,如果您必须这样做,那么以下任一模式都可能适合您。

  1. 分两个阶段执行批量插入,在正常导入之前,维护记录的映射或字典以及它们链接到的注释,然后在第一次调用 SaveChanges() 后,ID 将是可以插入。

  2. 您可以将映射的评论存储在实体的未绑定集合中,在 SaveChanges() 之后,如果此集合中有任何条目,则应使用新记录的 ID 插入它们。

让我们看看第一个选项:

var mappedComments = new Dictionary<Vehicle,Comment[]>();
// bulk processing,however you choose to do it
// importantly for each item,capture the record reference and the comments
foreach(var item in source)
{
    Vehicle newItem;
    ... construct/parse the new Entity object
    List<Comment> newComments = new List<Comment>();
    ... parse the comments records
    // store the map
    mappedComments.Add(newItem,newComments.ToArray());

    // Add the entity to the context?
    db.AddToVehicles(newItem);
}

db.SaveChanges();

foreach(var mapEntry in mappedComments)
{
    var newVehicle = mapEntry.Key;
    // replace this with your actual logic of course...
    int vehicleTableReferenceId = db.TableReferences.Single(x => x.TableName == nameof(Vehicle));
    foreach(var comment in mappEntry.Value)
    {
        comment.TableReferenceId = vehicleTableReferenceId;
        comment.EntityReferenceId = newVehicle.Id; // the Id that is now populated
        db.AddToComments(comment);
    }
}

db.SaveChanges();

如果您有很多实体类型表现出这种链接行为,那么您可以通过将映射注释嵌入实体本身来将此功能构建到实体本身中。

  1. 定义一个接口来描述对这些评论

    有弱引用的对象
    public interface ICommentsToInsert
    {
        // Only necessary if your convention is NOT to use a common name for the PK
        int Id { get; }
        ICollection<Comment> CommentsToInsert { get;set;}
    }
    
  2. 实现此接口并向实体添加一个未映射的集合属性,以存储要针对每条记录插入的评论条目

    partial class Vehicle : ICommentsToInsert
    {
        [NotMapped]
        int ICommentsToInsert.Id { get => Vehicle_Id; }
        [NotMapped]
        public ICollection<Comment> CommentsToInsert { get;set; } = new HashSet<Comment>();
    }
    
  3. 在您的批量逻辑中,将 Comment 记录添加到 Vehicle.CommentsToInsert 集合中,我会留给您...

  4. 覆盖 SaveChanges() 以检测具有评论的实体并在保存操作后重新处理它们。

    在这个例子中,我在保存之前为所有修改过的条目存储了EntityState,这对于这个特定的例子来说太过分了,但是你只会在保存过程中丢失这个状态信息,并记录下来对后处理逻辑的一系列其他应用程序变得非常有用。

    public override int SaveChanges()
    {
        var beforeStates = BeforeSaveChanges();
        int result = base.SaveChanges();
        if (AfterSaveChanges(beforeStates);
            result += base.SaveChanges();
        return results;
    }
    
    private Dictionary<DbEntityEntry,EntityState> BeforeSaveChanges()
    {
        var beforeSaveChanges = new Dictionary<DbEntityEntry,EntityState>();
        foreach( var entry in this.ChangeTracker.Entries())
        {
            //skip unchanged entries!
            if (entry.State == EntityState.Unchanged)
                continue;
            // Today,only cache the ICommentsToInsert records...
            if (entry.Entity is ICommentsToInsert)
                beforeSaveChanges.Add(entry,entry.State);
        }
        return beforeSaveChanges;
    }
    
    private bool AfterSaveChanges(Dictionary<DbEntityEntry,EntityState> statesBeforeSaveChanges)
    {
        bool moreChanges = false;
        foreach (var entry in statesBeforeChanges)
        {
            if (entry.Key.Entity is ICommentsToInsert hasComments)
            {
                if(hasComments.CommentsToInsert.Any())
                {
                    moreChanges = true;
                    // Get the Id to the TableReference,based on the name of the Entity type
                    // you would normally cache this type of lookup,rather than hitting the DB every time
                    int tableReferenceId = db.TableReferences.Single(x =
         > x.TableName == entry.Key.Entity.GetType().Name);
                    foreach (var comment in hasComments.CommentsToInsert)
                    {
                        comment.TableReferenceId = tableReferenceId;
                        comment.EntityReferenceId = hasComments.Id;
                        db.AddToComments(comment);
                    }
                }
            }
        }
        return moreChanges;
    }
    

您可以通过实现 DbTransaction 范围来进一步改进此功能,以便在出现故障时回滚全部内容,此代码本身是从我在生产代码中使用的常用例程转述而来的,因此虽然它可能无法正常工作是的,这个概念在很多项目中都让我受益匪浅。