
多対多の関係は、他の関係ほど単純ではありません。この記事では、多対多の関係を作成し、EF Core で使用する方法を紹介します。
モデル
多対多のシンプルで実用的な例として、何らかのデジタル E コマースストアを考えてみましょう。ユーザーは商品をカートに入れることができます(1 つのカートに複数の商品を入れられる)、そして商品は複数のカートに属します。最初に Cart クラスと Item クラスを作成しましょう。
public class Cart
{
public int Id { get; set; }
public ICollection<Item> Items { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
public ICollection<Cart> Carts { get; set; }
}
これは良さそうに見えますが、動作しません。この記事の執筆時点では、EF Core はこの状況を処理できません。EF Core はこの関係をどう扱えばよいか分からず、移行を追加しようとすると次のエラーが発生します。
Unable to determine the relationship represented by navigation property ‘Cart.Items’ of type ‘ICollection<Item>’. Either manually configure the relationship, or ignore this property using the ‘[NotMapped]’ attribute or by using ‘EntityTypeBuilder.Ignore’ in ‘OnModelCreating’.
【型 ‘ICollection<Item>’ のナビゲーションプロパティ ‘Cart.Items’ によって表されるリレーションシップを特定できません。リレーションシップを手動で構成するか、’[NotMapped]’ 属性を使用するか、’OnModelCreating’ で ‘EntityTypeBuilder.Ignore’ を使用してこのプロパティを無視してください。】
最初に行う必要があるのは、Cart と Item の多対多の関係を確立する別の「中間」クラス(テーブル)を手動で作成することです。このクラスを作成しましょう。
public class CartItem
{
public int CartId { get; set; }
public Cart Cart { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
}
Cart と Item を関連付ける新しいクラス CartItem を作成しました。さらに、それぞれのナビゲーションプロパティも変更する必要があります。
public class Cart
{
public int Id { get; set; }
public ICollection<CartItem> Items { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
public ICollection<CartItem> Carts { get; set; }
}
ここで移行を追加しようとすると、別のエラーが発生します。
The entity type ‘CartItem’ requires a primary key to be defined.
【エンティティ型 ‘CartItem’ には主キーを定義する必要があります。】
その通り、CartItem には主キーがありません。多対多の関係であるため、複合主キーを持つ必要があります。複合主キーは通常の主キーと似ていますが、1 つのプロパティ(列)ではなく 2 つのプロパティ(列)で構成されます。現在、複合キーを作成する唯一の方法は OnModelCreating 内で行うことです。
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<CartItem>().HasKey(i => new { i.CartId, i.ItemId });
}
最後に、データベース構造が EntityFramework によって処理できるようになり、移行を続行できます。
多対多への挿入
データベースに既に Cart と Item があると仮定します。特定の商品(Item)を特定のカート(Cart)に追加するには、新しい CartItem を作成して保存する必要があります。
var cart = db.Carts.First(i => i.Id == 256);
var item = db.Items.First(i => i.Id == 1024);
// 両方のクラスの主キーIDを使用して関連付けることができます
var cartItem = new CartItem
{
CartId = cart.Id,
ItemId = item.Id
};
// または、両方のクラスのエンティティを使用して関連付けることもできます
var cartItem = new CartItem
{
Cart = cart,
Item = item
};
db.Add(cartItem);
db.SaveChanges();
多対多での関連データの取得
データベースからデータを取得するのは非常に簡単です。Include を使用して関連データを取得することに注意してください。ここでは合計 3 つのテーブル(Cart、Item、CartItem(商品 Item とカート Cart を関連付ける))が関与します。
// すべての商品が関連付けられた特定のカートを取得する
var cartIncludingItems = db.Carts.Include(cart => cart.Items).ThenInclude(row => row.Item).First(cart => cart.Id == 1);
// 特定のカートのすべての商品を取得する
var cartItems = cartIncludingItems.Items.Select(row => row.Item);
また、リレーションシップを使用せずに実行できる操作もあります。たとえば、カート ID がある場合は、次の Linq を使用してすべての商品を一度に取得できます。
var cartId = 1;
var cartItems = db.Items.Where(item => item.Carts.Any(j => j.CartId == cartId));
同じ原則が逆のユースケースにも適用されます。つまり、上記のパターンを適用して、特定の商品を持つすべてのカートを取得できます。
多対多からの削除
削除とは、カート Cart と商品 Item の間の関係 CartItem を削除することを意味します。以下の例では、カート Cart や商品 Item 自体は削除せず、カート Cart と商品 Item の間の関係 CartItem のみを削除します。
最初に、カート Cart から単一の商品 Item を削除してみましょう。
var cartId = 1;
var itemId = 1;
var cartItem = db.CartsItems.First(row => row.CartId == cartId && row.ItemId == itemId);
db.Remove(cartItem);
db.SaveChanges();
次に、カートからすべての商品を削除する方法を示します。
var cart = db.Carts.Include(c=> c.Items).First(i => i.Id == 2);
db.RemoveRange(cart.Items);
db.SaveChanges();
原文著者:Zbigniew
原文タイトル:How to handle Many-To-Many in Entity Framework Core
原文リンク:https://softdevpractice.com/blog/many-to-many-ef-core/
翻訳:沙漠尽头的狼