19

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

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

例如,我有两个模型类:

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方法中编写什么代码来生成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" });
}

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();
}

此代码创建Parcel,BuyingItems以及它们的关系,但如果我BuyingItem在另一个中需要相同Parcel(它们具有多对多关系)并且我为第二个包裹重复此代码 - 它将BuyingItems在数据库中重复(尽管我设置了相同Id的 s )。

例子:

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();
}

如何BuyingItem在不同Parcel的 s 中添加相同的内容?

4

3 回答 3

19

您必须以与在任何 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不支持以任何方式更改关系,因此您不能使用它在下次迁移中添加或删除关系。如果您需要它,您必须通过加载和调用或导航集合来手动删除关系ParcelBuyingItems中断RemoveAdd添加新关系。

于 2011-12-18T11:47:57.947 回答
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");
        });
}

然后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 };
}

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

请注意,无论我们Add使用同一个项目对象调用该方法多少次,都不会导致主键违规。那是因为 EFHashSet<T>在内部使用来管理Parcel.Items集合。AHashSet<Item>就其性质而言,不会让您添加重复的项目。

此外,如果您设法绕过我在示例中演示的这种 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字段作为标识符表达式。将此属性添加到类的Description属性中Parcel以使其唯一:[MaxLength(255), Index(IsUnique=true)]. 更新方法中的以下代码段Seed

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

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

于 2014-08-18T14:32:56.497 回答
3

行。我了解在这种情况下我应该如何:

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,你给了我一个正确的向量来为我的任务找到一个解决方案。

于 2011-12-18T19:16:28.963 回答