Entity Framework Core での一対多(1:N)リレーションシップの設定は、データモデルの整合性とクエリ効率を左右する最も重要な構成要素の一つです。本稿では、Blog(一端)とPost(多端)を例に、外部キーの有無、ナビゲーションプロパティの存在可否、および必須/オプション制約に基づく8つの典型的な構成パターンを体系的に解説します。
基本的なエンティティ定義
まず、最小限のクリーンなエンティティクラスを定義します。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public virtual ICollection<Post> Articles { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public int? BlogId { get; set; }
public virtual Blog? OriginBlog { get; set; }
}
構成パターンと対応する Fluent API 設定
| 構成シナリオ | Fluent API 設定 |
|---|---|
両方のエンティティにナビゲーションプロパティあり、Post.BlogId が非nullで明示的外部キー |
builder.HasMany(b => b.Articles).WithOne(p => p.OriginBlog).HasForeignKey(p => p.BlogId).IsRequired(); |
| 同上だが、外部キーが null 許容(オプション関係) | builder.HasMany(b => b.Articles).WithOne(p => p.OriginBlog).HasForeignKey(p => p.BlogId).IsRequired(false); |
ナビゲーションのみ(外部キーなし)、EFがBlogIdをシャドウプロパティとして自動生成 |
builder.HasMany(b => b.Articles).WithOne(p => p.OriginBlog).IsRequired(); |
| 上記のオプション版(シャドウ外部キーが null 許容) | builder.HasMany(b => b.Articles).WithOne(p => p.OriginBlog).IsRequired(false); |
Blog 側のみナビゲーションあり、Post 側にはナビゲーションなし(外部キーは明示) |
builder.HasMany(b => b.Articles).WithOne().HasForeignKey(p => p.BlogId).IsRequired(); |
| 上記のオプション版 | builder.HasMany(b => b.Articles).WithOne().HasForeignKey(p => p.BlogId).IsRequired(false); |
| 両方ともナビゲーションなし — シャドウ外部キーのみ使用 | builder.HasMany<Post>().WithOne().HasForeignKey("BlogId").IsRequired(); |
| シャドウ外部キー+オプション関係(ナビゲーションなし) | builder.HasMany<Post>().WithOne().HasForeignKey("BlogId").IsRequired(false); |
シャドウプロパティによる外部キー操作
外部キーを明示的に定義しない場合、EF Core は内部的にBlogIdという名前のシャドウプロパティを生成します。この値は、EF.Property<T>(entity, "PropertyName") を用いてアクセス可能です。
// シャドウ外部キーを用いた条件付きインクルード
var blog = await context.Blogs
.Include(b => b.Articles.Where(p => EF.Property<int?>(p, "BlogId") == b.Id))
.FirstOrDefaultAsync(b => b.Id == 1);
構成クラスでの明示的シャドウプロパティ登録
外部キーをシャドウプロパティとして扱う場合は、Post の構成クラスで明示的に宣言します。
public class PostConfiguration : IEntityTypeConfiguration<Post>
{
public void Configure(EntityTypeBuilder<Post> builder)
{
builder.ToTable("Articles");
builder.HasKey(p => p.Id);
// シャドウ外部キーを明示的に定義
builder.Property<int>("BlogId");
}
}
データベースマイグレーションへの反映
必須関係(IsRequired())を指定すると、生成されるマイグレーションにはonDelete: ReferentialAction.Cascadeが含まれ、親レコード削除時に子レコードも連鎖削除されます。一方、IsRequired(false)ではReferentialAction.Restrict(またはDBMS依存のデフォルト)となり、削除制約が緩和されます。
// 必須関係の場合のマイグレーション断片(MySQL)
table.ForeignKey(
name: "FK_Articles_Blogs_BlogId",
column: x => x.BlogId,
principalTable: "Blogs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
プライベートコレクションとフィールドアクセスモード
ナビゲーションプロパティをプライベートフィールドで実装する場合、PropertyAccessMode.Field を適用して、EFが直接フィールドを読み書きできるようにします。
public class Blog
{
private readonly List<Post> _articles = new();
public virtual IReadOnlyCollection<Post> Articles => _articles.AsReadOnly();
}
// 構成内
builder.Property("_articles").UsePropertyAccessMode(PropertyAccessMode.Field);