EntityFramework Core 3.1 を使って、色々な一対多のリレーションを試してみた

前回、EntityFramework Core (以下EF Core)を使って、一対多のリレーションがあるデータを作成してみました。
ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた - メモ的な思考的な

 
そんな中、公式ドキュメントを読んでいると、いろんなパターンの一対多のリレーションを定義する方法が記載されていました。
リレーションシップ-EF Core | Microsoft Docs

ただ、EF CoreからどんなSQLDDL文が発行されるのか分からなかったことから、今回試してみることにしました。

 

目次

 

環境

前回の記事の環境を、今回も引き続き使います。

 

規約による定義(単一ナビゲーションプロパティ)

親クラスに単一ナビゲーションプロパティを定義するだけで、リレーションができるのは楽です。

ただ、裏側ではどんなDDLが発行されるのか気になったため、試してみます。

 

前回同様、Blog <-> Postでリレーションを作成するためのモデル

// Blog
public class BlogOfSingleNavProp
{
    public int Id { get; set; }
    public string Url { get; set; }
    
    // 単一ナビゲーションプロパティ(Single Navigation Property)のみ定義
    public List<PostOfSingleNavProp> PostsOfFk { get; set; }
}

// Post
public class PostOfSingleNavProp
{
    public int Id { get; set; }
    public string Content { get; set; }
}

を用意します。

次に、マイグレーションを作成・DBへ適用します。

# マイグレーションの作成
$ dotnet ef migrations add AddSingleNavigationProperty

# DBへの適用
$ dotnet ef database update

 
dotnet ef database update 時に発行されるDDLがログに出力されるため、内容を確認します。

Postにフィールド BlogOfSingleNavPropId が自動生成され、Blogへの外部キーが設定されました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "BlogOfSingleNavProps" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_BlogOfSingleNavProps" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "PostOfSingleNavProps" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_PostOfSingleNavProps" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogOfSingleNavPropId" INTEGER NULL,   # 自動生成された列
          CONSTRAINT "FK_PostOfSingleNavProps_BlogOfSingleNavProps_BlogOfSingleNavPropId" 
              FOREIGN KEY ("BlogOfSingleNavPropId") 
              REFERENCES "BlogOfSingleNavProps" ("Id") 
              ON DELETE RESTRICT
      );
      
# 自動生成されたPostのIndex
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_PostOfSingleNavProps_BlogOfSingleNavPropId" 
          ON "PostOfSingleNavProps" ("BlogOfSingleNavPropId");

 
スペースの関係上、以降のパターンは

のみを記載します。

 

Data Annotation によるリレーションの定義

Data Annotationによるリレーションは、2つの方法が用意されています。

  • [ForeignKey]
  • [InverseProperty]

ともに、データアノテーション名前空間 System.ComponentModel.DataAnnotations.Schema で定義されています。
https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.schema?view=netcore-3.1

今回はそれぞれ試してみます。

 

[ForeignKey] によるリレーション定義

[ForeignKey] の用途としては、主に外部キーの名前がEF Coreの命名規則と異なる場合に使うようです。

また、 [ForeignKey] は、以下のいずれかのパターンで付与します。

  • 依存エンティティ(子)の外部キーに付与
  • 依存エンティティ(子)の参照ナビゲーションプロパティに付与
  • プリンシパルエンティティ(親)の参照ナビゲーションプロパティに付与

なお、いずれも発行されるDDLは同じになります。

 

依存エンティティの外部キーに付与
public class Blog1
{
    public int Id { get; set; }
    public string Url { get; set; }
    public List<Post1> Posts { get; set; }
}

public class Post1
{
    public int Id { get; set; }
    public string Content { get; set; }

    // 依存エンティティの外部キーに付与
    [ForeignKey("Blog")]
    public int BlogFk { get; set; }
    
    // 参照ナビゲーションプロパティ
    public Blog1 Blog { get; set; }
}

 
結果です。 BlogFk の参照先が Blog.Id になっています。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog1List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

# Post      
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post1List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post1List_Blog1List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog1List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post1List_BlogFk" ON "Post1List" ("BlogFk");

 

依存エンティティの参照ナビゲーションプロパティに付与
public class Blog2
{
    public int Id { get; set; }
    public string Url { get; set; }
    public List<Post2> Posts { get; set; }
}

public class Post2
{
    public int Id { get; set; }
    public string Content { get; set; }

    // 外部キー
    public int BlogFk { get; set; }

    // 参照ナビゲーションプロパティに付与
    [ForeignKey("BlogFk")]
    public Blog2 Blog { get; set; }
}

 
こちらも同じ結果となりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog2List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

# Post      
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post2List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post2List_Blog2List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog2List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post2List_BlogFk" ON "Post2List" ("BlogFk");

 

プリンシパルエンティティの参照ナビゲーションプロパティに付与
public class Blog3
{
    public int Id { get; set; }
    public string Url { get; set; }
    
    // コレクションナビゲーションプロパティ
    [ForeignKey("BlogFk")]
    public List<Post3> Posts { get; set; }
}

public class Post3
{
    public int Id { get; set; }
    public string Content { get; set; }

    public int BlogFk { get; set; }
    public Blog3 Blog { get; set; }
}

 

こちらも同じ結果となりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog3List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post3List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post3List_Blog3List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog3List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post3List_BlogFk" ON "Post3List" ("BlogFk");

 

[InverseProperty] によるリレーション定義

[InverseProperty] の用途としては、一つの親クラスに対して、子クラスから複数の外部キーを定義する場合になります。

以下は、子の

  • Author
  • Contributor

という2つの項目が、それぞれ別の外部キー制約として定義する例です。

public class Post4
{
    public int Id { get; set; }
    public string Content { get; set; }

    public int AuthorId { get; set; }
    public User4 Author { get; set; }

    public int ContributorId { get; set; }
    public User4 Contributor { get; set; }
}

public class User4
{
    public int Id { get; set; }
    public string Name { get; set; }

    // Author向けのリレーション
    [InverseProperty("Author")]
    public List<Post4> AuthoredPosts { get; set; }
    
    // Contributor向けのリレーション
    [InverseProperty("Contributor")]
    public List<Post4> ContributedToPosts { get; set; }
}

 
結果です。

Postの

  • AuthorId
  • ContributorId

に対し、それぞれUserのIdが外部キー制約として設定されました。

# User
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "User4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_User4List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post4List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "AuthorId" INTEGER NOT NULL,
          "ContributorId" INTEGER NOT NULL,
          CONSTRAINT "FK_Post4List_User4List_AuthorId" 
              FOREIGN KEY ("AuthorId") 
              REFERENCES "User4List" ("Id") ON DELETE CASCADE,
          CONSTRAINT "FK_Post4List_User4List_ContributorId" 
              FOREIGN KEY ("ContributorId") 
              REFERENCES "User4List" ("Id") ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post4List_AuthorId" ON "Post4List" ("AuthorId");
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post4List_ContributorId" ON "Post4List" ("ContributorId");

 

外部キーで主キー以外のプロパティを参照する

主キーとは別のフィールドを、外部キーの参照先として設定したい場合です。

この時は、Fluent APIを使い、プリンシパルキープロパティを構成します。

プリンシパルキープロパティは、単一・複数あるため、それぞれ試してみます。

 

単一プリンシパルキーの構成

以下の例は、

  • 主キー:Blog5.Id
  • 外部キー:Post5.BlogUrl
  • 外部キーの参照先:Blog5.Url

として設定したい場合のモデルです。

public class Blog5
{
    public int Id { get; set; }      // 主キー
    public string Url { get; set; }  // 外部キーの参照先
    public string Description { get; set; }

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

public class Post5
{
    public int Id { get; set; }
    public string Title { get; set; }

    public string BlogUrl { get; set; }  // 外部キー
    public Blog5 Blog { get; set; }
}

 

Fluent APIは、DbContextの OnModelCreating() をオーバーライドして実装します。

modelBuilder.Entity<Post5>()
    .HasOne(m => m.Blog)  // 一側
    .WithMany(m => m.Posts)  // 多側
    .HasForeignKey(m => m.BlogUrl)  // 外部キーのフィールド
    .HasPrincipalKey(m => m.Url);   // 外部キーから参照する親のフィールド

 
結果です。

外部キーから参照する親のフィールド(Url)に対し、UNIQUE制約が追加されています。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog5List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "AK_Blog5List_Url"  // 制約が追加
              UNIQUE ("Url")
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post5List" PRIMARY KEY AUTOINCREMENT,
          "Title" TEXT NULL,
          "BlogUrl" TEXT NULL,
          CONSTRAINT "FK_Post5List_Blog5List_BlogUrl" 
              FOREIGN KEY ("BlogUrl") 
              REFERENCES "Blog5List" ("Url")  // 外部キーは親のUrl
              ON DELETE RESTRICT
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post5List_BlogUrl" ON "Post5List" ("BlogUrl");

 

複数プリンシパルキーの構成

複合プリンシパルキーは、複合外部キーとして親のフィールドを参照したい時に使います。

以下の例は、

  • 主キー:Blog6.Id
  • 複合外部キー
    • Post6.BlogUrlとPost6.Author
  • 複合外部キーの参照先
    • Post6.BlogUrl -> Blog6.Url
    • Post6.BlogAuthor -> Blog6.Author

として設定したい場合のモデルです。

public class Blog6
{
    public int Id { get; set; }         // 主キー
    public string Url { get; set; }     // BlogUrlの参照先
    public string Author { get; set; }  // BlogAuthorの参照先
    public string Description { get; set; }

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

public class Post6
{
    public int Id { get; set; }
    public string Title { get; set; }

    public string BlogUrl { get; set; }     // 複合外部キー
    public string BlogAuthor { get; set; }  // 複合外部キー
    public Blog6 Blog { get; set; }
}

 
単一の場合と同様、Fluent APIにて構成します。

modelBuilder.Entity<Post6>()
    .HasOne(m => m.Blog)
    .WithMany(m => m.Posts)
    .HasForeignKey(m => new {m.BlogUrl, m.BlogAuthor})
    .HasPrincipalKey(m => new {m.Url, m.Author});

 
結果です。

Blogには UNIQUE ("Url", "Author") という制約が追加されています。

Postにも、 FOREIGN KEY ("BlogUrl", "BlogAuthor") という外部キー制約が追加されています。

また、複合外部キーの場合、Indexの追加はありませんでした。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog6List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog6List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "AK_Blog6List_Url_Author" 
              UNIQUE ("Url", "Author")
          
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post6List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post6List" PRIMARY KEY AUTOINCREMENT,
          "Title" TEXT NULL,
          "BlogUrl" TEXT NULL,
          "BlogAuthor" TEXT NULL,
          CONSTRAINT "FK_Post6List_Blog6List_BlogUrl_BlogAuthor" 
              FOREIGN KEY ("BlogUrl", "BlogAuthor") 
              REFERENCES "Blog6List" ("Url", "Author") 
              ON DELETE RESTRICT
      );
      
# Index:なし

 

ナチュラルキーの主キーに対する、外部キー制約

EF Coreでは、主キーをサロゲートキーではなくナチュラルキーでも定義できます。

そこで、ナチュラルキーの主キーに対する外部キー制約も試してみます。

 

単一外部キーの場合

以下のモデルは

  • 主キーは、Blog・Postともにナチュラルキー
    • Blogは Url
    • Postは Title
  • Postの外部キー( UrlFk )は、Blogの主キー(ナチュラルキー)である Url を参照している

という定義です。

public class NkBlog1
{
    [Key]
    public string Url { get; set; }

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

public class NkPost1
{
    [Key]
    public string Title { get; set; }
    public string Content { get; set; }

    [ForeignKey("Blog")]
    public string UrlFk { get; set; }
    public NkBlog1 Blog { get; set; }
}

 
このモデルをマイグレーションすると、以下のDDLが発行されました。

  • Blogの Url が主キーになる
  • Postの UrlFk がBlogの Url を参照した外部キーになる

ということが分かります。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkBlog1List" (
          "Url" TEXT NOT NULL CONSTRAINT "PK_NkBlog1List" PRIMARY KEY
      );

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkPost1List" (
          "Title" TEXT NOT NULL CONSTRAINT "PK_NkPost1List" PRIMARY KEY,
          "Content" TEXT NULL,
          "UrlFk" TEXT NULL,
          CONSTRAINT "FK_NkPost1List_NkBlog1List_UrlFk" 
              FOREIGN KEY ("UrlFk") 
              REFERENCES "NkBlog1List" ("Url") 
              ON DELETE RESTRICT
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_NkPost1List_UrlFk" ON "NkPost1List" ("UrlFk");

 

複合外部キーの場合

以下のモデルを使って

  • 主キーは、Blog・Postともにナチュラルキー
    • Blogは UrlAuthor
    • Postは UrlAuthorTitle
  • Postの外部キーは2つ(UrlとAuthor)で、Blogの主キー(ナチュラルキー)であるUrlとAuthorを参照している

を定義したいケースです。

public class NkBlog2
{
    public string Url { get; set; }     // 複合主キー(ナチュラルキー)
    public string Author { get; set; }  // 複合主キー(ナチュラルキー)

    public string Description { get; set; }
    
    public List<NkPost2> Posts { get; set; }

}

public class NkPost2
{
    public string Url { get; set; }     // 複合主キー(ナチュラルキー & 外部キー)
    public string Author { get; set; }  // 複合主キー(ナチュラルキー & 外部キー)
    public string Title { get; set; }   // 複合主キー(ナチュラルキー、これだけ外部キー参照なし)

    public string Content { get; set; }
    
    public NkBlog2 Blog { get; set; }
}

 
この場合、Fluent APIを使ってDbContextに定義します。

まずは、Blogの複合主キー(ナチュラルキー)の定義です。

// Blogの複合主キー(ナチュラルキー)
modelBuilder.Entity<NkBlog2>()
    .HasKey(m => new {m.Url, m.Author});

 
続いて、Blogの複合主キーを参照する、Postの複合外部キーの定義です。

// Postの複合外部キー
modelBuilder.Entity<NkPost2>()
    .HasOne(p => p.Blog)
    .WithMany(m => m.Posts)
    .HasForeignKey(m => new {m.Url, m.Author});

 
最後に、Postの複合主キーを定義します。

// Postの複合主キー
modelBuilder.Entity<NkPost2>()
    .HasKey(m => new {m.Url, m.Author, m.Title});

 
結果です。

Blogは CONSTRAINT "PK_NkBlog2List" PRIMARY KEY ("Url", "Author") と、主キーが複合ナチュラルキーになりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkBlog2List" (
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "PK_NkBlog2List" PRIMARY KEY ("Url", "Author")
      );

 
Postは

  • 複合主キー: PRIMARY KEY ("Url", "Author", "Title"),
  • 複合外部キー: FOREIGN KEY ("Url", "Author")

と定義されました。

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkPost2List" (
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Title" TEXT NOT NULL,
          "Content" TEXT NULL,
          CONSTRAINT "PK_NkPost2List" 
              PRIMARY KEY ("Url", "Author", "Title"),
          CONSTRAINT "FK_NkPost2List_NkBlog2List_Url_Author"
              FOREIGN KEY ("Url", "Author") 
              REFERENCES "NkBlog2List" ("Url", "Author") 
              ON DELETE CASCADE
      );

なお、Indexの定義はありませんでした。

 

ソースコード

GitHubに上げました。

  • Models/BlogPostOfFkDataAnnotation.cs
  • Models/BlogPostOfInversePropDataAnnotation.cs
  • Models/BlogPostOfPrincipal.cs
  • Models/BlogPostOfSingleNavProp.cs
  • Models/NkBlogPostOfFk.cs
  • Models/NkBlogPostOfMultiFk.cs
  • Models/MyContext.cs

あたりが今回のファイルです。
https://github.com/thinkAmi-sandbox/ASP_Net_Core-sample