如何在实体框架迁移中为具有多对多关系的数据建立种子

Dmi*_*kov 18 entity-framework ef-code-first ef-migrations

我使用实体框架迁移(在自动迁移模式下).一切都很好,但我有一个问题:当我有多对多的关系时,我应该如何播种数据.例如,我有两个模型类:

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; }
}
Run Code Online (Sandbox Code Playgroud)

我理解如何为简单数据(对于PaymentSystem类)和一对多关系建立种子,但是我应该在Seed方法中编写什么代码来生成Parcel和BuyingItem的一些实例?我的意思是使用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" });
}
Run Code Online (Sandbox Code Playgroud)
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();
}
Run Code Online (Sandbox Code Playgroud)

是.这段代码创建了Parcel,BuyingItems和relation,但是如果我在其他Parcel中需要相同的BuyingItem(它们有多对多的关系),如果我为第二个parcel重复这个代码 - 它将在数据库中复制BuyingItems(尽管我设置了)同样的Id).例:

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();
}
Run Code Online (Sandbox Code Playgroud)

如何在不同的包中添加相同的BuyingItem?

Lad*_*nka 20

您必须以与在任何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();
}
Run Code Online (Sandbox Code Playgroud)

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

AddOrUpdate不支持以任何方式更改关系,因此您无法在下次迁移中使用它来添加或删除关系.如果你需要它,你必须手动通过装载删除的关系ParcelBuyingItems和调用RemoveAdd在导航集打破或添加新的关系.

  • 如果BuyingItem.Id是标识列,这似乎不能按预期工作.在这种情况下,当项目不存在时,将插入一个由DB生成的ID而不是您指定的ID.这意味着具有硬编码ID的AddOrUpdate不是播种的可行解决方案,因为它将在下一次种子运行时生成重复项.在EF5上测试过. (7认同)
  • -1这是错误的并且错误地接受了答案:手动设置的ID被忽略,因此这将无法解决问题.OP有一个答案,下面有正确的解决方案. (3认同)

Rav*_*tel 19

更新的答案

请务必阅读下面的"正确使用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");
        });
}
Run Code Online (Sandbox Code Playgroud)

然后像这样实现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 };
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,无论我们Parcel使用相同的项目对象调用该方法多少次,我们最终都不会遇到主键违规.这是因为EF内部用于BuyingItem管理Add集合.A HashSet<T>,就其性质而言,不会让您添加重复的项目.

此外,如果你以某种方式管理这个EF行为,就像我在示例中演示的那样,我们的主键不会让重复的内容.

正确使用AddOrUpdate

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

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

考虑以下代码,

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 }
);
Run Code Online (Sandbox Code Playgroud)

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

这里的真正解决方案是use AddOrUpdatefield作为标识符表达式 将此属性添加到类的AddOrUpdate属性Seed以使其唯一Description.更新Description方法中的以下片段:

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");
Run Code Online (Sandbox Code Playgroud)

注意,我没有使用该Parcel字段,因为EF在插入行时会忽略它.我们正在使用它[MaxLength(255), Index(IsUnique=true)]来检索正确的宗地对象,无论它是什么Seed价值.


老答案

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

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

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

Tom Dykstra的这篇教程向我展示了正确的做法.这是有效的,因为我们不认为任何理所当然.我们不指定ID.相反,我们通过已知的唯一键查询上下文,并将相关实体(通过查询上下文再次获取)添加到它们.在我的案例中,它就像一个魅力.