【C#】Entity Framework Coreで関連テーブルのデータをまとめて取得する方法

目次

概要

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() を追加するとパフォーマンスが向上します。

注意点

  1. N+1問題の防止: Include を忘れて foreach ループ内で関連データ(ここでは department.Employees)にアクセスしようとすると、Lazy Loading(遅延読み込み)が無効な設定の場合、データが空になったり例外が発生したりします。Lazy Loadingが有効な場合でも、ループの回数分SQLが発行され性能が悪化します。
  2. カルテシアン爆発: 複数のコレクションに対して Include を重ねると(例: EmployeesとProjectsとAssets…)、結合によってデータ量が爆発的に増え、パフォーマンスが低下する恐れがあります。必要な関連データのみを取得するようにしてください。
  3. 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回に抑えられます。

必要な関連データのみを明示的にロードすることで、アプリケーションのパフォーマンスとデータの整合性を保ちやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次