前回、EntityFramework Core (以下EF Core)を使って、一対多のリレーションがあるデータを作成してみました。
ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた - メモ的な思考的な
そんな中、公式ドキュメントを読んでいると、いろんなパターンの一対多のリレーションを定義する方法が記載されていました。
リレーションシップ-EF Core | Microsoft Docs
ただ、EF CoreからどんなSQLのDDL文が発行されるのか分からなかったことから、今回試してみることにしました。
目次
- 環境
- 規約による定義(単一ナビゲーションプロパティ)
- Data Annotation によるリレーションの定義
- 外部キーで主キー以外のプロパティを参照する
- ナチュラルキーの主キーに対する、外部キー制約
- ソースコード
環境
前回の記事の環境を、今回も引き続き使います。
規約による定義(単一ナビゲーションプロパティ)
親クラスに単一ナビゲーションプロパティを定義するだけで、リレーションができるのは楽です。
ただ、裏側ではどんな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では、主キーをサロゲートキーではなくナチュラルキーでも定義できます。
そこで、ナチュラルキーの主キーに対する外部キー制約も試してみます。
単一外部キーの場合
以下のモデルは
という定義です。
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は
Url
とAuthor
- Postは
Url
とAuthor
とTitle
- Blogは
- 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