web-dev-qa-db-ja.com

EntityFrameworkの移行で多対多の関係を持つデータをシードする方法

エンティティフレームワークの移行を使用しています(自動移行モード)。すべて大丈夫ですが、1つ質問があります。

多対多の関係がある場合、どのようにデータをシードする必要がありますか?

たとえば、2つのモデルクラスがあります。

_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クラスの場合)と1対多の関係をシードする方法を理解していますが、SeedParcelのインスタンスを生成するには、BuyingItemメソッドにどのコードを記述する必要がありますか? _Update-Database_を実行するたびにデータを複製したくないので、DbContext.AddOrUpdate()を使用することを意味します。

_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とそれらの関係を作成しますが、別のBuyingItemで同じParcelが必要な場合(多対多の関係があります)、2番目の区画でこのコードを繰り返します-データベース内で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();
}
_

同じBuyingItemを異なるParcelsに追加するにはどうすればよいですか?

18
Dmitry Gorshkov

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をロードし、ナビゲーションコレクションでRemoveまたはAddを呼び出して新しいリレーションを解除または追加することにより、リレーションを手動で削除する必要があります。

20
Ladislav Mrnka

更新された回答

完全な回答については、以下の「AddOrUpdateを適切に使用する」セクションを必ずお読みください。

まず、重複を排除するために、複合主キー(区画IDとアイテム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を使用して、必要な小包を取得し、アイテムオブジェクトを購入します。その後、ItemsオブジェクトのParcelプロパティ(コレクション)を使用して、必要に応じてBuyingItemを追加します。

同じアイテムオブジェクトを使用してAddメソッドを何度呼び出しても、主キー違反が発生することはないことに注意してください。これは、EFが内部でHashSet<T>を使用してParcel.Itemsコレクションを管理しているためです。 HashSet<Item>は、その性質上、重複するアイテムを追加することはできません。

さらに、例で示したように、このEF動作を回避するために何らかの方法で管理する場合、主キーは重複を許可しません。

AddOrUpdateを適切に使用する

AddOrUpdateメソッドで識別子式として一般的なIdフィールド(int、identity)を使用する場合は、注意が必要です。

この場合、Parcelテーブルから行の1つを手動で削除すると、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");

EFは行の挿入中にそれを無視するため、私はIdフィールドを使用していないことに注意してください。また、Descriptionの値に関係なく、Idを使用して正しいパーセルオブジェクトを取得しています。


古い答え

ここにいくつかの所見を追加したいと思います。

  1. Id列がデータベースで生成されたフィールドである場合、Idを使用してもおそらく効果はありません。 EFはそれを無視します。

  2. このメソッドは、Seedメソッドを1回実行すると正常に機能しているようです。ただし、2回実行しても重複は作成されません(ほとんどの場合、これを実行する必要があります)。多くの場合)、重複を注入する可能性があります。私の場合はそうしました。

このチュートリアル トム・ダイクストラによる正しいやり方を教えてくれました。当たり前のことを何も考えていないので、うまくいきます。 IDは指定しません。代わりに、既知の一意のキーを使用してコンテキストをクエリし、関連するエンティティ(コンテキストをクエリすることで取得されます)をそれらに追加します。私の場合、それは魅力のように機能しました。

19
Ravi M Patel

OK。私はその状況でどうあるべきかを理解しています:

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

データベースに重複はありません。

ありがとう、ラディスラフ、あなたは私の仕事の解決策を見つけるための正しいベクトルを私にくれました。

3
Dmitry Gorshkov