【问题标题】:How to seed data with many-to-may relations in Entity Framework Migrations如何在实体框架迁移中使用多对五关系播种数据
【发布时间】:2023-04-09 13:39:02
【问题描述】:

我使用实体框架迁移(在自动迁移模式下)。一切都很好,但我有一个问题:

当我有多对多关系时,我应该如何播种数据?

例如,我有两个模型类:

public class Parcel
{
    public int Id { get; set; }
    public string Description { get; set; }
    public double Weight { get; set; }
    public virtual ICollection<BuyingItem> Items { get; set; }
}

public class BuyingItem
{
    public int Id { get; set; }
    public decimal Price { get; set; }
    public virtual ICollection<Parcel> Parcels { get; set; }
}

我了解如何播种简单数据(用于 PaymentSystem 类)和一对多关系,但是我应该在 Seed 方法中编写什么代码来生成 ParcelBuyingItem 的一些实例?我的意思是使用DbContext.AddOrUpdate(),因为我不想每次运行Update-Database时都复制数据。

protected override void Seed(ParcelDbContext context)
{
    context.AddOrUpdate(ps => ps.Id,
        new PaymentSystem { Id = 1, Name = "Visa" },
        new PaymentSystem { Id = 2, Name = "PayPal" },
        new PaymentSystem { Id = 3, Name = "Cash" });
}

protected override void Seed(Context context)
{
    base.Seed(context);

    // This will create Parcel, BuyingItems and relations only once
    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.SaveChanges();
}

此代码创建ParcelBuyingItems 及其关系,但如果我在另一个Parcel 中需要相同的BuyingItem(它们具有多对多关系),我将重复此代码第二次parcel - 它将在数据库中复制BuyingItems(尽管我设置了相同的Ids)。

例子:

protected override void Seed(Context context)
{
    base.Seed(context);

    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.AddOrUpdate(new Parcel() 
    { 
        Id = 2, 
        Description = "Test2", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.SaveChanges();
}

如何在不同的Parcels 中添加相同的BuyingItem

【问题讨论】:

    标签: entity-framework ef-code-first entity-framework-migrations


    【解决方案1】:

    您必须以与在任何 EF 代码中构建多对多关系相同的方式填充多对多关系:

    protected override void Seed(Context context)
    {
        base.Seed(context);
    
        // This will create Parcel, BuyingItems and relations only once
        context.AddOrUpdate(new Parcel() 
        { 
            Id = 1, 
            Description = "Test", 
            Items = new List<BuyingItem>
            {
                new BuyingItem() { Id = 1, Price = 10M },
                new BuyingItem() { Id = 2, Price = 20M }
            }
        });
    
        context.SaveChanges();
    }
    

    指定将在数据库中使用的Id 至关重要,否则每个Update-Database 都会创建新记录。

    AddOrUpdate 不支持以任何方式更改关系,因此您不能使用它在下一次迁移中添加或删除关系。如果需要,您必须手动删除关系,方法是使用 BuyingItems 加载 Parcel 并在导航集合上调用 RemoveAdd 以中断或添加新关系。

    【讨论】:

    • AddOrUpdate 不支持在 EF 迁移最终版本中更改关系仍然有效吗?它会解释这个问题stackoverflow.com/q/10474839/270591 中的问题,不是吗?
    • 如果 BuyingItem.Id 是一个标识列,这似乎无法按预期工作。在这种情况下,当该项目不存在时,它会使用由 DB 生成的 ID 而不是您指定的 ID 插入。这意味着带有硬编码 ID 的 AddOrUpdate 不是播种的可行解决方案,因为它会在下一次播种运行时生成重复项。在 EF5 上测试。
    • Ladislav,当根实体和相关实体已经存在于数据库中时(简而言之,当我们'试图简单地更新关系)。调用Parcel.Items.Add(new BuyingItem() { Id = 1, Price = 10M }) 时,未按调用context.SaveChanges() 后的预期添加项目。
    • 我想 Id 是标识列,将在 dbms 中生成。在这种情况下,您无法将其设置为您想要的值。因此,无法确保 EF 再次运行播种时不会出现重复,因为您无法控制 Id。例如,您有 3 个地块(parcel1、parcel2、parcel3)并且您从播种中删除了 parcel2。如果您决定再次创建数据库或在其他实例上运行您的应用程序,您将重复 parcel3。当种子代码中使用的 Id 与 db 中的 Id 不同步时,就会出现问题。
    • -1 因为这是错误的并且被错误地接受了答案:手动设置的 id 被忽略,所以这将不起作用并解决问题。 OP 在下面给出了正确解决方案的答案。
    【解决方案2】:

    好的。我明白在这种情况下我应该如何:

    protected override void Seed(Context context)
    {
        base.Seed(context);
        var buyingItems = new[]
        {
            new BuyingItem
            {
                 Id = 1,
                 Price = 10m
            },
            new BuyingItem
            {
                 Id = 2,
                 Price = 20m,
            }
        }
    
        context.AddOrUpdate(new Parcel() 
        { 
            Id = 1, 
            Description = "Test", 
            Items = new List<BuyingItem>
            {
                buyingItems[0],
                buyingItems[1]
            }
        },
        new Parcel() 
        { 
            Id = 2, 
            Description = "Test2", 
            Items = new List<BuyingItem>
            {
                buyingItems[0],
                buyingItems[1]
            }
        });
    
        context.SaveChanges();
    }
    

    数据库中没有重复项。

    谢谢你,Ladislav,你给了我一个正确的向量来为我的任务找到一个解决方案。

    【讨论】:

      【解决方案3】:

      更新答案

      请务必阅读下面的“正确使用 AddOrUpdate”部分以获得完整答案。

      首先,让我们创建一个复合主键(由parcel id和item id组成)以消除重复。在DbContext类中添加如下方法:

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
          base.OnModelCreating(modelBuilder);
      
          modelBuilder.Entity<Parcel>()
              .HasMany(p => p.Items)
              .WithMany(r => r.Parcels)
              .Map(m =>
              {
                  m.ToTable("ParcelItems");
                  m.MapLeftKey("ParcelId");
                  m.MapRightKey("BuyingItemId");
              });
      }
      

      然后像这样实现Seed 方法:

      protected override void Seed(Context context)
      {
          context.Parcels.AddOrUpdate(p => p.Id,
              new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
              new Parcel { Id = 2, Description = "Parcel 2", Weight = 2.0 },
              new Parcel { Id = 3, Description = "Parcel 3", Weight = 3.0 });
      
          context.BuyingItems.AddOrUpdate(b => b.Id,
              new BuyingItem { Id = 1, Price = 10m },
              new BuyingItem { Id = 2, Price = 20m });
      
          // Make sure that the above entities are created in the database
          context.SaveChanges();
      
          var p1 = context.Parcels.Find(1);
          // Uncomment the following line if you are not using lazy loading.
          //context.Entry(p1).Collection(p => p.Items).Load();
      
          var p2 = context.Parcels.Find(2);
          // Uncomment the following line if you are not using lazy loading.
          //context.Entry(p2).Collection(p => p.Items).Load();
      
          var i1 = context.BuyingItems.Find(1);
          var i2 = context.BuyingItems.Find(2);
      
          p1.Items.Add(i1);
          p1.Items.Add(i2);
      
          // Uncomment to test whether this fails or not, it will work, and guess what, no duplicates!!!
          //p1.Items.Add(i1);
          //p1.Items.Add(i1);
          //p1.Items.Add(i1);
          //p1.Items.Add(i1);
          //p1.Items.Add(i1);
      
          p2.Items.Add(i1);
          p2.Items.Add(i2);
      
          // The following WON'T work, since we're assigning a new collection, it'll try to insert duplicate values only to fail.
          //p1.Items = new[] { i1, i2 };
          //p2.Items = new[] { i2 };
      }
      

      这里我们通过在Seed 方法中调用context.SaveChanges() 来确保在数据库中创建/更新实体。之后,我们使用context 检索所需的包裹和购买项目对象。此后,我们使用Parcel 对象上的Items 属性(它是一个集合)随意添加BuyingItem

      请注意,无论我们使用同一个项目对象调用Add 方法多少次,都不会导致主键违规。这是因为 EF 在内部使用 HashSet&lt;T&gt; 来管理 Parcel.Items 集合。 HashSet&lt;Item&gt; 本质上不会让您添加重复项。

      此外,如果您设法绕过我在示例中演示的这种 EF 行为,我们的主键不会让重复项进入。

      正确使用AddOrUpdate

      当您使用典型的 Id 字段(int、identity)作为带有AddOrUpdate 方法的标识符表达式时,您应该谨慎行事。

      在这种情况下,如果您从 Parcel 表中手动删除其中一行,则每次运行 Seed 方法时最终都会创建重复项(即使使用我上面提供的更新的Seed 方法)。

      考虑以下代码,

      context.Parcels.AddOrUpdate(p => p.Id,
          new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
          new Parcel { Id = 2, Description = "Parcel 1", Weight = 1.0 },
          new Parcel { Id = 3, Description = "Parcel 1", Weight = 1.0 }
      );
      

      从技术上讲(考虑到这里的代理 ID),行是唯一的,但从最终用户的角度来看,它们是重复的。

      这里真正的解决方案是使用Description 字段作为标识符表达式。将此属性添加到Parcel 类的Description 属性以使其唯一:[MaxLength(255), Index(IsUnique=true)]。在Seed 方法中更新以下sn-ps:

      context.Parcels.AddOrUpdate(p => p.Description,
          new Parcel { Description = "Parcel 1", Weight = 1.0 },
          new Parcel { Description = "Parcel 2", Weight = 2.0 },
          new Parcel { Description = "Parcel 3", Weight = 3.0 });
      
      // Make sure that the above entities are created in the database
      context.SaveChanges();
      
      var p1 = context.Parcels.Single(p => p.Description == "Parcel 1");
      

      注意,我没有使用 Id 字段,因为 EF 在插入行时会忽略它。我们使用Description 来检索正确的包裹对象,不管Id 的值是什么。


      旧答案

      我想在这里补充几点意见:

      1. 如果 Id 列是数据库生成的字段,则使用 Id 可能不会有任何好处。 EF 会忽略它。

      2. Seed 方法运行一次时,此方法似乎工作正常。它不会创建任何重复项,但是,如果您第二次运行它(我们大多数人必须经常这样做),它可能会注入重复项。就我而言,确实如此。

      This tutorial Tom Dykstra 向我展示了正确的做法。它之所以有效,是因为我们不认为任何事情是理所当然的。我们不指定 ID。相反,我们通过已知的唯一键查询上下文并向它们添加相关实体(再次通过查询上下文获取)。在我的情况下,它就像一个魅力。

      【讨论】:

      • 我用了上面教程中的方法,效果很好。
      • 标记的答案不考虑身份字段主键。 Tom Dykstra 教程中的 Seed 方法确实如此,因此它是一个更强大的解决方案。
      • 请考虑在您的答案中添加更多细节,而不仅仅是一个链接。
      • @mikesigs 谢谢,我已经更新了我的答案。现在够了吗?
      • 完美。很多人不明白使用Id作为identifierExpression的陷阱。现在希望有人会来将此标记为答案!
      猜你喜欢
      • 1970-01-01
      • 2020-09-12
      • 1970-01-01
      • 2021-12-28
      • 1970-01-01
      • 2013-09-16
      • 1970-01-01
      • 2018-09-27
      • 2013-08-30
      相关资源
      最近更新 更多