前回、EntityFramework Core (以下EF Core)を使って、一対多のリレーションがあるデータを作成してみました。
ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた - メモ的な思考的な
そんな中、公式ドキュメントを読んでいると、いろんなパターンの一対多のリレーションを定義する方法が記載されていました。
リレーションシップ-EF Core | Microsoft Docs
ただ、EF CoreからどんなSQLのDDL文が発行されるのか分からなかったことから、今回試してみることにしました。
目次
環境
前回の記事の環境を、今回も引き続き使います。
規約による定義(単一ナビゲーションプロパティ)
親クラスに単一ナビゲーションプロパティを定義するだけで、リレーションができるのは楽です。
ただ、裏側ではどんなDDLが発行されるのか気になったため、試してみます。
前回同様、Blog <-> Postでリレーションを作成するためのモデル
public class BlogOfSingleNavProp
{
public int Id { get; set; }
public string Url { get; set; }
public List<PostOfSingleNavProp> PostsOfFk { get; set; }
}
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]
の用途としては、一つの親クラスに対して、子クラスから複数の外部キーを定義する場合になります。
以下は、子の
という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; }
[InverseProperty("Author")]
public List<Post4> AuthoredPosts { get; set; }
[InverseProperty("Contributor")]
public List<Post4> ContributedToPosts { get; set; }
}
結果です。
Postの
に対し、それぞれ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; }
public string Author { get; set; }
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ともにナチュラルキー
- 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は
Url
と Author
- Postは
Url
と Author
と Title
- 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の複合主キー(ナチュラルキー)の定義です。
modelBuilder.Entity<NkBlog2>()
.HasKey(m => new {m.Url, m.Author});
続いて、Blogの複合主キーを参照する、Postの複合外部キーの定義です。
modelBuilder.Entity<NkPost2>()
.HasOne(p => p.Blog)
.WithMany(m => m.Posts)
.HasForeignKey(m => new {m.Url, m.Author});
最後に、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