【问题标题】:Insertion order of multiple records in Entity Framework实体框架中多条记录的插入顺序
【发布时间】:2022-03-12 17:09:00
【问题描述】:

当我尝试一次添加具有多个子级的实体时,我遇到了 EF 重新排序插入的问题。我有一个 3 级结构,每个结构之间都有一对多的关系(Outer 1--* Item 1--* SubItem)。如果我尝试插入带有项目和子项目的新外层,则最终会首先插入包含子项目的项目。

示例代码(.NET 4.5、EF 5.0.0-rc):

public class Outer
{
    public int OuterId { get; set; }
    public virtual IList<Item> Items { get; set; }
}

public class Item
{
    public int OuterId { get; set; }
    [ForeignKey("OuterId")]
    public virtual Outer Outer { get; set; }

    public int ItemId { get; set; }
    public int Number { get; set; }

    public virtual IList<SubItem> SubItems { get; set; }
}

public class SubItem
{
    public int SubItemId { get; set; }

    [ForeignKey("ItemId")]
    public virtual Item Item { get; set; }
    public int ItemId { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Outer> Outers { get; set; }
    public DbSet<Item> Items { get; set; }
    public DbSet<SubItem> SubItems { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
        MyContext context = new MyContext();

        // Add an Outer object, with 3 Items, the middle one having a subitem
        Outer outer1 = new Outer { Items = new List<Item>() };
        context.Outers.Add(outer1);
        outer1.Items.Add(new Item { Number = 1, SubItems = new List<SubItem>() });
        outer1.Items.Add(new Item { Number = 2, SubItems = new List<SubItem>(new SubItem[] { new SubItem() }) });
        outer1.Items.Add(new Item { Number = 3, SubItems = new List<SubItem>() });

        context.SaveChanges();

        // Print the order these have ended up in
        foreach (Item item in context.Items)
        {
            Console.WriteLine("{0}\t{1}", item.ItemId, item.Number);
        }
        // Produces output:
        // 1       2
        // 2       1
        // 3       3
    }
}

我知道this answer by Alex James 指出插入可能需要重新排序以满足关系约束,但这不是这里的问题。他的回答还提到他们无法跟踪列表等保持顺序的结构中的项目顺序。

我想知道如何订购这些插件。虽然我可以依靠 PK 以外的字段对插入的项目进行排序,但如果我可以依靠 PK 顺序,效率会高得多。我真的不想使用多个 SaveChanges 调用来完成此操作。

我正在使用 EF5 RC,但从其他未回答的问题来看,这已经存在一段时间了!

【问题讨论】:

    标签: entity-framework entity-framework-5


    【解决方案1】:

    我想知道如何订购这些插件。

    你不能。数据库命令的顺序是 EF 的内部行为。如果您想控制命令的顺序,请不要使用将您从低级数据库交互中抽象出来的工具 - 直接使用 SQL。

    根据评论编辑:

    是的,这是低级交互,因为在使用不受您控制的抽象时,您会期望 SQL 命令的顺序。在高层次上,您会得到不同的东西,因为您使用的期望不适用于该抽象。如果您想控制 SQL 命令的顺序,您必须通过一项一项保存项目(=> 多个 SaveChangesTransactionScope)来强制执行 EF,或者自己编写 SQL。否则使用单独的列进行排序。

    顺便说一句。如您所见,EF 不会保存实体。它有自己的更改跟踪器,保存对所有附加实例的引用。引用保存在多个Dictionary 实例和字典doesn't preserve insertion order 中。如果这些集合用于生成 SQL 命令(我猜它们是),则无法保证顺序。

    【讨论】:

    • 我不认为这是低级交互。我要求存储一个有序的集合,它没有这样做。在高层次上,我得到的东西与我投入的不同。
    • 我直接在答案中添加了一些解释。
    • 您可以做很多事情,但需要了解框架。您可以在上下文的部分类中处理保存,分配您自己的更改跟踪器,通过上下文编写 SQL 查询,直接绑定 sproc 并将结果集作为 EF 生成的对象,或者修改 tt 文件以获得更多模块化。听起来在这种情况下只是简单地覆盖了 SaveChanges()。查看以下结果集,这是在我的部分上下文中: IEnumerable modifiedOrAddedEntities = this.ChangeTracker.Entries().Where(x => x.State == EntityState.Added);
    【解决方案2】:

    数据库中的表是集合。这意味着订单无法保证。我假设在您的示例中您想要按“数字”排序的结果。如果这是您想要的,如果该数字发生变化并且不再反映数据库中的顺序,您会怎么做?

    如果您真的希望以特定顺序插入行,最好选择多个 SaveChanges。

    没有人愿意多次调用 SaveChanges 的原因是因为这感觉就是它的本来面目:一个肮脏的 hack。

    由于主键是一个技术概念,因此在此键上对结果进行排序应该没有任何功能意义。您可以按特定字段对结果进行排序,并为此使用数据库索引。您可能不会看到速度上的差异。

    明确排序还有其他好处: 对于必须维护它的人来说更容易理解。否则该人必须知道主键排序很重要并给出正确的结果,因为在您的应用程序的另一个(完全)不相关的部分中,它意外地与数字字段的顺序相同。

    【讨论】:

    • 我的问题不是我是否应该使用主键进行排序——我只是希望能够做到这一点。我的例子被大大简化了,实际的实现类似于日志文件——随着时间的推移添加条目,并且永远不会更新或删除。是的,我可以将索引粘贴到另一列(本例中为 Number),但这意味着我使用 2 个索引来完成可以用 1 完成的事情。
    • 在这种情况下,除了多次调用 SaveChanges 之外,您无能为力。
    【解决方案3】:

    我找到了一种方法。它只是想我会让你知道:

    using (var dbContextTransaction = dbContext.Database.BeginTransaction())
    {
      dbContext.SomeTables1.Add(object1);
      dbContext.SaveChanges();
    
      dbContext.SomeTables1.Add(object2);
      dbContext.SaveChanges();
    
      dbContextTransaction.Commit();
    }
    

    【讨论】:

    • -1:保存系列中的每个单独项目是 EF 的反模式。 .AddRange() 或 AddRangeAsync() 应改为使用 (source)。在 n=2 时,我们看不到性能影响,但在 n=1000 时可能会产生重大影响。
    【解决方案4】:

    另一种方法是通过实体状态更改的组合,在添加每个条目后无需数据库往返(虽然很大程度上依赖于应用程序逻辑)。

    在我的例子中 - 节点层次结构 - 我必须先保留根节点,然后再保留层次结构的其余部分,以便自动路径计算工作。

    所以我有一个 root 节点,没有提供父 ID,而子节点提供了父 ID。

    EF Core 随机(或通过复杂和智能的逻辑 - 根据您的喜好 :) 随机调度节点用于插入、中断路径计算过程。

    所以我采用了覆盖上下文的 SaveChanges 方法并检查集合中的实体,我需要为其维护某些插入顺序 - 首先分离任何子节点,然后保存更改,附加子节点并再次保存更改。

    // select child nodes first - these entites should be added last
    List<EntityEntry<NodePathEntity>> addedNonRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == true).ToList();
    
    // select root nodes second - these entities should be added first
    List<EntityEntry<NodePathEntity>> addedRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == false).ToList();
    
            if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedRoots))
            {
                if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedNonRoots))
                {
                    // detach child nodes, so they will be ignored on SaveChanges call
                    // no database inserts will be generated for them
                    addedNonRoots.ForEach(e => e.State = EntityState.Detached);
    
                    // run SaveChanges - since root nodes are still there, 
                    // in ADDED state, inserts will be executed for these entities
                    int detachedRowCount = base.SaveChanges();
    
                    // re-attach child nodes to the context
                    addedNonRoots.ForEach(e => e.State = EntityState.Added);
    
                    // run SaveChanges second time, child nodes are saved
                    return base.SaveChanges() + detachedRowCount;
                }
            }
    

    这种方法不允许您保留单个实体的顺序,但如果您可以将实体分类为必须先插入的实体,以及可以稍后插入的实体 - 这个 hack 可能会有所帮助。

    【讨论】:

      【解决方案5】:

      保存之前的多个 Add() 或保存之前的 AddRange() 不会保留顺序。此外,当您阅读集合时,不能保证以与最初添加的顺序相同的顺序返回结果。您需要为您的实体添加一些属性,并在查询时使用 OrderBy() 以确保它们按您想要的顺序返回。

      【讨论】:

        【解决方案6】:

        这不专业。但是,我用这种方法解决了我的问题。

        PublicMenu.cs 文件:

        public class PublicMenu
            {
                public int Id { get; set; }
        
                public int? Order { get; set; }
                
                public int? ParentMenuId { get; set; }
        
                [ForeignKey("ParentMenuId")]
                public virtual PublicMenu Parent { get; set; }
        
                public virtual ICollection<PublicMenu> Children { get; set; }
        
                public string Controller { get; set; }
        
                public string Action { get; set; }
        
                public string PictureUrl { get; set; }
        
                public bool Enabled { get; set; }
        }
        

        PublicMenus.json 文件

        [
            {
                "Order": 1,
                "Controller": "Home",
                "Action": "Index",
                "PictureUrl": "",
                "Enabled": true
            },
            {
                "Order": 2,
                "Controller": "Home",
                "Action": "Services",
                "PictureUrl": "",
                "Enabled": true
            },
            {
                "Order": 3,
                "Controller": "Home",
                "Action": "Portfolio",
                "PictureUrl": "",
                "Enabled": true
            },
            {
                "Order": 4,
                "Controller": "Home",
                "Action": "About",
                "PictureUrl": "",
                "Enabled": true
            },
            {
                "Order": 5,
                "Controller": "Home",
                "Action": "Contact",
                "PictureUrl": "",
                "Enabled": true
            }
        ]
        

        DataContextSeed 文件

            public class DataContextSeed
                {
                    public static async Task SeedAsync(ICMSContext context, ILoggerFactory loggerFactory)
                    {
                        try
                        {
                               if (!context.PublicMenus.Any())
                                {
                                    var publicMenusData = File.ReadAllText("../Infrastructure/Data/SeedData/PublicMenus.json");
                                    var publicMenus = JsonSerializer.Deserialize<List<PublicMenu>>(publicMenusData);
                                    foreach (var item in publicMenus)
                                    {
                                        context.PublicMenus.Add(item);
                                        await context.SaveChangesAsync();
                                    }
                               }
                        }
                        catch (Exception ex)
                        {
                            var logger = loggerFactory.CreateLogger<ICMSContextSeed>();
                            logger.LogError(ex.Message);
                        }
                    }
           }
        

        【讨论】:

          【解决方案7】:

          废话!

          这个问题是 9 年前提出的,微软仍然没有做任何事情。

          在EF6中,我发现如果我使用

          for(int i=0; i<somearray.Length; i++)
          db.Table.Add(new Item {Name = somearray[i]});
          

          它是按顺序执行的,但如果我使用

          foreach(var item in somearry)
          db.Table.Add(new Item {Name = item})
          

          那么就没有顺序了。

          我在使用EF core 3.1的时候,总是出现上述两种情况的顺序不一样。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-08-09
            • 2015-04-19
            • 1970-01-01
            • 1970-01-01
            • 2016-10-31
            相关资源
            最近更新 更多