前回は、EF Core のクエリでデータを取得する方法を見ました。
今回は、関連データの読み込みを扱います。
リレーショナルデータベースでは、テーブル同士が外部キーで関連します。EF Core では、それをナビゲーションプロパティとして扱えます。
ただし、関連データをいつ読み込むかは重要です。
必要なデータを読み込まないと画面表示に困りますし、読み込みすぎると性能問題になります。
サンプルモデル
車と注文の関係を使います。
public sealed class Car
{
public int Id { get; set; }
public string Make { get; set; } = "";
public string Color { get; set; } = "";
public string PetName { get; set; } = "";
public List<Order> Orders { get; set; } = new();
}
public sealed class Order
{
public int Id { get; set; }
public int CarId { get; set; }
public string CustomerName { get; set; } = "";
public DateTime OrderDate { get; set; }
public Car? Car { get; set; }
}
設定です。
modelBuilder.Entity<Car>()
.HasMany(car => car.Orders)
.WithOne(order => order.Car)
.HasForeignKey(order => order.CarId);
Car から複数の Order へ進めます。
Order から 1 台の Car へ戻れます。
関連データは自動では常に読み込まれない
次のように Cars だけを取得します。
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
この時点で、Orders が必ず読み込まれているとは限りません。
EF Core では、関連データの読み込み方法を選びます。
主な方法は 3 つです。
- 最初から一緒に読み込む
- 必要になった時点で明示的に読み込む
- プロパティアクセス時に自動で読み込む
それぞれ見ていきます。
Include で一緒に読み込む
最初から関連データが必要だと分かっている場合は、Include() を使います。
Car? car = await context.Cars
.Include(car => car.Orders)
.FirstOrDefaultAsync(car => car.Id == 1);
これで、車とその注文が一緒に読み込まれます。
一覧でも使えます。
List<Car> cars = await context.Cars
.AsNoTracking()
.Include(car => car.Orders)
.OrderBy(car => car.Id)
.ToListAsync();
Include() は便利ですが、関連データが多いと結果が大きくなります。
本当に必要な関連だけを読み込みましょう。
ThenInclude でさらに深い関連を読み込む
関連のさらに先を読み込む場合は、ThenInclude() を使います。
List<Order> orders = await context.Orders
.Include(order => order.Car)
.ThenInclude(car => car.Features)
.ToListAsync();
深い関連を読み込むほど、生成される SQL や結果セットが複雑になります。
表示に必要な形が決まっているなら、DTO へ投影する方が効率的な場合もあります。
投影で関連データを必要な形にする
画面表示では、エンティティ全体ではなく必要な情報だけあれば十分なことが多いです。
var cars = await context.Cars
.AsNoTracking()
.Select(car => new
{
car.Id,
car.PetName,
OrderCount = car.Orders.Count
})
.ToListAsync();
このように投影すると、必要な列と集計だけを取得できます。
Include() より SQL が軽くなることがあります。
明示的に読み込む
最初は親だけ取得し、必要になったら関連を読み込む方法です。
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
if (car != null)
{
await context.Entry(car)
.Collection(car => car.Orders)
.LoadAsync();
}
参照ナビゲーションの場合は Reference() を使います。
Order? order = await context.Orders
.FirstOrDefaultAsync(order => order.Id == 1);
if (order != null)
{
await context.Entry(order)
.Reference(order => order.Car)
.LoadAsync();
}
明示的な読み込みは、「条件によって関連が必要かどうか変わる」場面で便利です。
関連データに条件を付けて読み込む
明示的な読み込みでは、関連データに対してクエリを組むこともできます。
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
if (car != null)
{
List<Order> recentOrders = await context.Entry(car)
.Collection(car => car.Orders)
.Query()
.Where(order => order.OrderDate >= DateTime.UtcNow.AddDays(-30))
.ToListAsync();
}
全件読み込まず、必要な関連だけを取得できます。
遅延読み込み
遅延読み込みは、ナビゲーションプロパティへアクセスしたタイミングで関連データを読み込む仕組みです。
利用には追加設定が必要です。
dotnet add package Microsoft.EntityFrameworkCore.Proxies
設定例です。
optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer(connectionString);
ナビゲーションプロパティは virtual にします。
public sealed class Car
{
public int Id { get; set; }
public string Make { get; set; } = "";
public string Color { get; set; } = "";
public string PetName { get; set; } = "";
public virtual List<Order> Orders { get; set; } = new();
}
これで、Orders にアクセスしたときに読み込まれる可能性があります。
Car car = await context.Cars.FirstAsync();
foreach (Order order in car.Orders)
{
Console.WriteLine(order.CustomerName);
}
遅延読み込みの注意点
遅延読み込みは便利ですが、SQL がいつ実行されるか見えにくくなります。
たとえば、車一覧を取得して、それぞれの注文へアクセスするコードです。
List<Car> cars = await context.Cars.ToListAsync();
foreach (Car car in cars)
{
Console.WriteLine(car.Orders.Count);
}
この場合、最初に車一覧を取る SQL が 1 回、各車の注文を取る SQL が車の件数分実行される可能性があります。
これを N+1 問題と呼びます。
関連データが必要だと分かっているなら、Include() や投影を使う方が安全です。
どれを選ぶべきか
使い分けの目安です。
最初から必要なら Include
詳細画面など、親と子が必ず必要な場合に向いています。
context.Cars.Include(car => car.Orders)
条件付きで必要なら明示的な読み込み
ユーザー操作や条件によって関連が必要になる場合に向いています。
context.Entry(car).Collection(car => car.Orders).LoadAsync()
画面表示なら投影
必要な列だけ取りたい場合に向いています。
context.Cars.Select(car => new CarListItem { ... })
遅延読み込みは慎重に
便利ですが、SQL の発行タイミングが見えにくくなります。
チーム開発や高負荷なアプリでは、意図しない N+1 問題の原因になることがあります。
まとめ
EF Core では、関連データの読み込み方法を選べます。
Include()、明示的な読み込み、遅延読み込みには、それぞれ向いている場面と注意点があります。
関連データは「とりあえず全部読む」ではなく、「画面や処理に必要な形だけ読む」と考えると、性能と可読性のバランスが取りやすくなります。
次回は、取得したデータの更新と削除を扱います。