プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2018/01/11

Entity Framework Core におけるリレーションシップについて

event_note2018/01/10 15:16

公式サイトに Entity Framework Core のリレーションシップについての記述がありますが、私のような初心者にはなかなか難しいので、和訳を兼ねて自分なりにまとめてみました。

しかし、後半に行くほどただ和訳しただけになってしまいました・・・。

環境

  • Visual Studio 2017
  • .NET Core 2.0
  • Entity Framework Core 2.0

用語

リレーションシップについて説明するにあたり、いろいろな用語が出てくるので、公式サイトではまず用語の定義から説明されています。

稚拙な訳ですが、とりあえずまとめてみました。

  • Dependent entity
    外部キーのプロパティを含んだエンティティ。リレーションシップにおける「子」になる。
  • Principal entity
    プライマリ/代替キーのプロパティを含むエンティティ。リレーションシップにおける「親」になる。
  • Foreign key (外部キー)
    関連する Principal entity のキーを格納するための Dependent entity 内のプロパティ。
  • Principal key
    Principal entity の主キーまたは代替キー。
  • Navigation property
    Principal entity と Dependent entity の両方またはどちらかに定義される、関連するエンティティへの参照を含むプロパティ。
    • Collection navigation property
      多くの関連するエンティティへの参照を含む Navigation property。
    • Reference navigation property
      単一の関連エンティティへの参照を保持する Navigation property。
    • Inverse navigation property
      特定のナビゲーションプロパティについて説明するときおいて、リレーションシップのもう一方の Navigation property のこと。

以下にコードと用語の関係を示します。

// Blog is the principal entity
public class Blog
{
    // Blog.BlogId is the principal key
    // (in this case it is a primary key rather than an alternate key)
    public int BlogId { get; set; }
    public string Url { get; set; }

    // Blog.Posts is a collection navigation property
    public List<Post> Posts { get; set; }
}

// Post is the dependent entity
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
 
    // Post.BlogId is the foreign key
    public int BlogId { get; set; }
    // Post.Blog is a reference navigation property
    // Post.Blog is the inverse navigation property of Blog.Posts (and vice versa)
    public Blog Blog { get; set; }
}

Conventions

※ Convention だと意味がよくわかりませんが、ここでは EF Core の規則とでも言えばよいのでしょうかね。

Convention により、ある型に Navigation property があった場合にリレーションシップが作られます。 そのプロパティの指す型が現在のデータベースプロバイダーによってスカラ型としてマップされない場合、プロパティは Navigation property と見なされます。

Convention によるリレーションシップは、常に Principal entity の主キーを対象とします。 代替キーをターゲットにするには、Fluent API を使用して追加の設定を行う必要があります。

リレーションシップの完全な定義

最も一般的なリレーションシップのパターンは、そのリレーションシップの両端に定義された Navigation property と、Dependent entity クラス内に定義された外部キーを持つことです。

これは上記のコードの例で言えば、以下のことを指します。

  • Blog クラス内に Navigation property として Posts を持つ
  • Post クラス内に Navigation property として Blog を持つ
  • Post クラス内に 外部キーとして BlogId を持つ

2つの型の間に Navigation property のペアがある場合、それらのプロパティはそのリレーションシップにおける Inverse navigation property として互いに設定されます。

Dependent entity に以下のいずれかの名前のプロパティがあれば、それは外部キーとして設定されます。

  • <primary key property name>
  • <navigation property name><primary key property name>
  • <principal entity name><primary key property name>

上記の例で言えば、Post クラス内に、BlogId または BlogBlogId というプロパティがあればそれが外部キーとみなされることになり、BlogId が存在するため、これが外部キーとして設定されます。

もし2つの型の間に Navigation property が複数あれば、Convention によってリレーションシップは作成されないため、手動で設定する必要があります。

外部キーがない場合

Dependent entity 内には外部キーのプロパティを持つことが推奨されますが、必須ではありません。
もし外部キーのプロパティがなければ、<navigation property name><principal key property name> という隠れた外部キーが用意されます。 (詳細は Shadow Property を参照)

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    // 外部キーなし
    public Blog Blog { get; set; }
}

Single Navigation Property

1つの Navigation property だけを持つ場合(Inverse navigation と外部キーのプロパティがない場合)でも、Convention によってリレーションシップを持つことができます。 単一の Navigation property と外部キーを持つこともできます。

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    // 単一の Navigation property だけを持つ
    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}

Cascade Delete

※ Cascade Delete は直訳すると連鎖的な削除といった感じでしょうか?

Convention により、cascade delete は、必須のリレーションシップに対しては Cascade に、オプションのリレーションシップに対しては ClientSetNull に設定されます。

Cascade とは、依存するエンティティも削除されることを意味します。 ClientSetNull とは、メモリーにロードされない Dependent entity は変更されないため、手動で削除するか、有効な principal entity を指すように更新する必要があることを意味します。 メモリーにロードされたエンティティの場合、EF Core は外部キーのプロパティを null にセットしようとします。

必須のリレーションシップとオプションのリレーションシップについては Required and Optional Relationships を参照してください。

Convention によって行われる削除の振る舞いの違いについては、Cascade Delete を参照してください。

Data Annotations

リレーションシップを設定には、[ForeignKey][InverseProperty] の2つの Data Annotations があります。

[ForeignKey]

Data Annotations を使用して、特定のリレーションシップの外部キープロパティーとして使用するプロパティーを設定することができます。
これは通常、外部キーのプロパティが Conventions によって検出されない場合に実行されます。

// Blog is the principal entity
public class Blog
{
    // Blog.BlogId is the principal key (in this case it is a primary key rather than an alternate key)
    public int BlogId { get; set; }
    public string Url { get; set; }

    // Blog.Posts is a collection navigation property
    public List<Post> Posts { get; set; }
}

// Post is the dependent entity
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogForeignKey { get; set; }

    // Post.Blog is a reference navigation property
    [ForeignKey("BlogForeignKey")]
    public Blog Blog { get; set; }
}

[ForeignKey] はリレーションシップ内のどこかの Navigation property に配置できます。 dependent entity クラス内の Navigation property に行く必要はありません。

[InverseProperty]

You can use the Data Annotations to configure how navigation properties on the dependent and principal entities pair up. This is typically done when there is more than one pair of navigation properties between two entity types.

Data Annotations を使用して、Dependent entity と Principal entity に Navigation property のペアを設定できます。
これは通常、2つのエンティティの間に1つ以上の Navigation property のペアがある場合に実行されます。

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int AuthorUserId { get; set; }
    public User Author { get; set; }

    public int ContributorUserId { get; set; }
    public User Contributor { get; set; }
}

public class User
{
    public string UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [InverseProperty("Author")]
    public List<Post> AuthoredPosts { get; set; }

    [InverseProperty("Contributor")]
    public List<Post> ContributedToPosts { get; set; }
}

Fluent API

Fluent API でリレーションシップを設定するためには、リレーションシップを構成する Navigation property を指定します。
HasOne または HasMany は、設定を開始するエンティティの Navigation property を指定します。
その後、WithOne または WithMany を呼び出して、Inverse navigation property を指定します。

HasOneWithOne は Navigation property に対して使用され、HasManyWithMany は collection navigation properties に対して使用されます。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

Single Navigation Property

Navigation property が1つしかない場合、 WithOneWithMany には引数なしのオーバーロードがあります。
これは、リレーションシップのもう一方の端には、概念的に参照またはコレクションがあることを示しますが、エンティティクラスには Navigation property は含まれていません。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne();
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}

Foreign Key

Fluent APIを使用して、特定のリレーションシップの外部キープロパティーとして使用するプロパティーを設定できます。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogForeignKey);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogForeignKey { get; set; }
    public Blog Blog { get; set; }
}

以下のコードは複合外部キーを設定する方法です。

class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Car>()
            .HasKey(c => new { c.State, c.LicensePlate });

        modelBuilder.Entity<RecordOfSale>()
            .HasOne(s => s.Car)
            .WithMany(c => c.SaleHistory)
            .HasForeignKey(s => new { s.CarState, s.CarLicensePlate });
    }
}

public class Car
{
    public string State { get; set; }
    public string LicensePlate { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }

    public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
    public int RecordOfSaleId { get; set; }
    public DateTime DateSold { get; set; }
    public decimal Price { get; set; }

    public string CarState { get; set; }
    public string CarLicensePlate { get; set; }
    public Car Car { get; set; }
}

HasForeignKey(...) の文字列オーバーロードを使用して、Shadow property を外部キーとして構成できます。 (詳細は、Shadow Properties を参照)。

外部キーとして使用する前に、Shadow property をモデルに明示的に追加することを推奨します(下記参照)。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Add the shadow property to the model
        modelBuilder.Entity<Post>()
            .Property<int>("BlogForeignKey");

        // Use the shadow property as a foreign key
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey("BlogForeignKey");
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

Principal Key

外部キーが主キー以外のプロパティーを参照するようにするには、Fluent API を使用してリレーションシップの Principal key のプロパティーを指定します。
Principal key として設定したプロパティは、自動的に代替キーとして設定されます。
(詳細については、Alternate Keys を参照してください)。

class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<RecordOfSale>()
            .HasOne(s => s.Car)
            .WithMany(c => c.SaleHistory)
            .HasForeignKey(s => s.CarLicensePlate)
            .HasPrincipalKey(c => c.LicensePlate);
    }
}

public class Car
{
    public int CarId { get; set; }
    public string LicensePlate { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }

    public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
    public int RecordOfSaleId { get; set; }
    public DateTime DateSold { get; set; }
    public decimal Price { get; set; }

    public string CarLicensePlate { get; set; }
    public Car Car { get; set; }
}

次のコードは、Principal key の複合キーを指定する方法を示しています。

class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<RecordOfSale>()
            .HasOne(s => s.Car)
            .WithMany(c => c.SaleHistory)
            .HasForeignKey(s => new { s.CarState, s.CarLicensePlate })
            .HasPrincipalKey(c => new { c.State, c.LicensePlate });
    }
}

public class Car
{
    public int CarId { get; set; }
    public string State { get; set; }
    public string LicensePlate { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }

    public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
    public int RecordOfSaleId { get; set; }
    public DateTime DateSold { get; set; }
    public decimal Price { get; set; }

    public string CarState { get; set; }
    public string CarLicensePlate { get; set; }
    public Car Car { get; set; }
}

Principal key のプロパティーを指定する順序は、それらが外部キーに指定されている順序と一致していなければなりません。

Required and Optional Relationships

Fluent API を使用して、リレーションシップが必須かオプションかを設定できます。
最終的には、外部キーのプロパティが必須かオプションかどうかで制御します。
これは、隠れた状態の外部キーを使用している場合に最も便利です。(?)
エンティティクラスに外部キーのプロパティがある場合、リレーションシップの必要性に応じて、外部キーのプロパティが必須かオプションかが判断されます。
(詳細は、Required and Optional properties を参照)。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .IsRequired();
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

Cascade Delete

Fluent APIを使用すると、特定のリレーションシップの連鎖的な削除処理を明示的に設定できます。
各オプションの詳細については、Saving Data セクションの Cascade Delete を参照してください。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Other Relationship Patterns

一対一

一対一のリレーションシップでは、両方に Reference navigation property があります。
それらは1対多のリレーションシップと同じ規則に従いますが、1つの Dependent entity だけが各 Principal entity に関連していることを保証するために、外部キープロパティに固有のインデックスが導入されています。(?)

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

EF は、外部キーのプロパティを検出できるかどうかに基づいて、エンティティの1つを Dependent entity として選択します。(?)
間違ったエンティティが Dependent entity として選択された場合は、Fluent API を使用して修正することができます。

Fluent API でリレーションシップを設定するときは、HasOne メソッドと WithOne メソッドを使用します。

外部キーを設定するときは、Dependent entity の型を指定する必要があります。
以下のコードの HasForeignKey に提供されている汎用パラメータに注意してください。

一対多のリレーションシップでは、Reference navigation property を持つエンティティが Dependent entity であり、コレクションを持つエンティティが Principal entity であることは明らかです。
しかし、一対一のリレーションシップではそのようなことはないため、明示的に定義する必要があります。

class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<BlogImage> BlogImages { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasOne(p => p.BlogImage)
            .WithOne(i => i.Blog)
            .HasForeignKey<BlogImage>(b => b.BlogForeignKey);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogForeignKey { get; set; }
    public Blog Blog { get; set; }
}

多対多

結合テーブルを表すエンティティクラスがない多対多のリレーションシップは、まだサポートされていません。 ただし、多対多のリレーションシップは、結合テーブルのエンティティクラスを組み込み、2つの異なる一対多のリレーションシップをマッピングすることで表現できます。

class MyContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PostTag>()
            .HasKey(t => new { t.PostId, t.TagId });

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Post)
            .WithMany(p => p.PostTags)
            .HasForeignKey(pt => pt.PostId);

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Tag)
            .WithMany(t => t.PostTags)
            .HasForeignKey(pt => pt.TagId);
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}