目次
概要
Entity Framework Core (EF Core) を使用して、主となるデータ(親)とそれに関連付くデータ(子)を一度のクエリで効率的に取得する実装です。 Include メソッドを使用することで、データベースへのラウンドトリップを減らし、「N+1問題」を回避しつつ関連データをロードします。
仕様(入出力)
- 入力: 検索対象の部門名(文字列)。
- 出力: 部門情報と、その部門に所属する従業員のリストをコンソールに表示。
- 前提: .NET 6.0以上、Microsoft.EntityFrameworkCore.InMemory(動作確認用)。
基本の使い方
Microsoft.EntityFrameworkCore 名前空間の Include 拡張メソッドを使用します。
// "Sales" 部門を取得し、関連する Employees も一緒にロードする
var department = await context.Departments
.Include(d => d.Employees) // ここで結合を指定
.FirstOrDefaultAsync(d => d.Name == "Sales");
// データアクセス後、Employeesプロパティにアクセス可能
foreach (var emp in department.Employees)
{
Console.WriteLine(emp.Name);
}
コード全文
このコードは「部門(Department)」と「従業員(Employee)」の1対多のリレーションを想定しています。 動作確認のためにインメモリデータベースを使用しているため、そのままコピーして実行可能です。
外部ライブラリとして Microsoft.EntityFrameworkCore.InMemory が必要です。
dotnet add package Microsoft.EntityFrameworkCore.InMemory
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public class Program
{
public static async Task Main()
{
// データベースのセットアップ(インメモリ動作)
var options = new DbContextOptionsBuilder<CompanyDbContext>()
.UseInMemoryDatabase(databaseName: "CompanyDb")
.Options;
// データシード(初期データの投入)
using (var context = new CompanyDbContext(options))
{
if (!await context.Departments.AnyAsync())
{
var dept = new Department { Name = "Development" };
context.Departments.Add(dept);
context.Employees.AddRange(
new Employee { Name = "Alice", Role = "Engineer", Department = dept },
new Employee { Name = "Bob", Role = "Designer", Department = dept },
new Employee { Name = "Charlie", Role = "Manager", Department = dept }
);
await context.SaveChangesAsync();
}
}
// データの取得と表示
using (var context = new CompanyDbContext(options))
{
var worker = new CompanyDataWorker(context);
await worker.ShowDepartmentDetailsAsync("Development");
}
}
}
// 業務ロジッククラス
public class CompanyDataWorker
{
private readonly CompanyDbContext _context;
public CompanyDataWorker(CompanyDbContext context)
{
_context = context;
}
public async Task ShowDepartmentDetailsAsync(string deptName)
{
// Includeを使って関連データをEager Loading(即時ロード)
var department = await _context.Departments
.Include(d => d.Employees)
.SingleOrDefaultAsync(d => d.Name == deptName);
if (department == null)
{
Console.WriteLine($"部門 '{deptName}' は見つかりませんでした。");
return;
}
Console.WriteLine($"部門: {department.Name}");
Console.WriteLine("----------------------------");
foreach (var employee in department.Employees)
{
Console.WriteLine($"- {employee.Name} ({employee.Role})");
}
}
}
// エンティティ定義
public class Department
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// ナビゲーションプロパティ
public List<Employee> Employees { get; set; } = new();
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public int DepartmentId { get; set; }
public Department? Department { get; set; }
}
// DbContext定義
public class CompanyDbContext : DbContext
{
public CompanyDbContext(DbContextOptions<CompanyDbContext> options)
: base(options) { }
public DbSet<Department> Departments => Set<Department>();
public DbSet<Employee> Employees => Set<Employee>();
}
実行結果例
部門: Development
----------------------------
- Alice (Engineer)
- Bob (Designer)
- Charlie (Manager)
カスタムポイント
- 条件付きロード:
Include(d => d.Employees.Where(e => e.Role == "Engineer"))のように記述することで、関連データの一部だけをフィルタリングして取得できます(EF Core 5.0以降)。 - 孫データの取得: 関連データのさらにその関連データを取得する場合は、
.Include(...).ThenInclude(...)を使用してください。 - 読み取り専用: データの表示のみで更新を行わない場合、
.AsNoTracking()を追加するとパフォーマンスが向上します。
注意点
- N+1問題の防止:
Includeを忘れてforeachループ内で関連データ(ここではdepartment.Employees)にアクセスしようとすると、Lazy Loading(遅延読み込み)が無効な設定の場合、データが空になったり例外が発生したりします。Lazy Loadingが有効な場合でも、ループの回数分SQLが発行され性能が悪化します。 - カルテシアン爆発: 複数のコレクションに対して
Includeを重ねると(例: EmployeesとProjectsとAssets…)、結合によってデータ量が爆発的に増え、パフォーマンスが低下する恐れがあります。必要な関連データのみを取得するようにしてください。 - Nullチェック:
SingleOrDefaultAsyncはデータが見つからない場合にnullを返します。直後のアクセス前に必ずNullチェックを行ってください。
応用
孫テーブル(階層が深いデータ)まで取得する場合の記述例です。
// 例: 部門 -> 従業員 -> 従業員の所有PC情報 まで取得
var data = await _context.Departments
.Include(d => d.Employees)
.ThenInclude(e => e.PcAssignment) // Employeeに関連するPC情報を取得
.FirstOrDefaultAsync(d => d.Id == 1);
まとめ
階層が深くなる場合は ThenInclude を活用してください。
親子関係にあるデータを扱う際は、Include メソッドを使うことでSQLの発行回数を1回に抑えられます。
必要な関連データのみを明示的にロードすることで、アプリケーションのパフォーマンスとデータの整合性を保ちやすくなります。
