前回は、Entity Framework Core の役割と ADO.NET との違いを見ました。
今回は、EF Core の基本部品を整理します。
特に重要なのは、DbContext、DbSet<T>、変更追跡、エンティティです。
DbContext は作業単位
DbContext は、EF Core におけるデータベース操作の中心です。
役割は大きく分けて次の通りです。
- データベース接続の設定を持つ
- モデルとテーブルの対応付けを管理する
- クエリを実行する
- 取得したオブジェクトの変更を追跡する
SaveChanges()で変更を保存する
using Microsoft.EntityFrameworkCore;
public sealed class AutoLotContext : DbContext
{
public AutoLotContext(DbContextOptions<AutoLotContext> options)
: base(options)
{
}
public DbSet<Car> Cars => Set<Car>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>().ToTable("Inventory");
modelBuilder.Entity<Order>().ToTable("Orders");
}
}
DbContext は長く使い回すものではありません。
Web アプリでは 1 リクエストごと、コンソールアプリでは 1 つの作業単位ごとに作る、と考えると扱いやすいです。
DbSet はテーブルに対応する入口
DbSet<T> は、特定のエンティティ型の集合を表します。
public DbSet<Car> Cars => Set<Car>();
Cars に対して LINQ を書くと、EF Core が SQL に変換します。
List<Car> hondas = await context.Cars
.Where(car => car.Make == "Honda")
.ToListAsync();
追加、更新、削除の入口にもなります。
context.Cars.Add(new Car
{
Make = "Honda",
Color = "Blue",
PetName = "Vega"
});
await context.SaveChangesAsync();
DbSet<T> は、テーブルそのものというより「その型を問い合わせたり変更したりする入口」と考えると自然です。
エンティティとは
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; }
}
CarId は外部キーです。
Car はナビゲーションプロパティです。
リレーション設定です。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.ToTable("Inventory")
.HasMany(car => car.Orders)
.WithOne(order => order.Car)
.HasForeignKey(order => order.CarId);
}
変更追跡の考え方
EF Core は、取得したエンティティの状態を追跡します。
Car? car = await context.Cars.FindAsync(1);
if (car != null)
{
car.PetName = "New Name";
await context.SaveChangesAsync();
}
このコードでは、Update 文を明示的に書いていません。
EF Core が car.PetName の変更を検知し、SaveChangesAsync() 時に必要な SQL を発行します。
ChangeTracker で状態を見る
ChangeTracker を使うと、EF Core が追跡しているエンティティ状態を確認できます。
Car car = new Car
{
Make = "Toyota",
Color = "Black",
PetName = "Night"
};
context.Cars.Add(car);
foreach (var entry in context.ChangeTracker.Entries())
{
Console.WriteLine($"{entry.Entity.GetType().Name}: {entry.State}");
}
代表的な状態は次の通りです。
Added
新規追加され、まだデータベースへ保存されていない状態です。
Unchanged
データベースから取得され、変更されていない状態です。
Modified
取得後に変更された状態です。
Deleted
削除予定の状態です。
Detached
コンテキストに追跡されていない状態です。
状態を理解すると、SaveChanges() が何を実行するのか見通しやすくなります。
所有される型
エンティティとして独立させるほどではない値のまとまりを、所有される型として扱えます。
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address Address { get; set; } = new();
}
public sealed class Address
{
public string PostalCode { get; set; } = "";
public string City { get; set; } = "";
public string Street { get; set; } = "";
}
設定です。
modelBuilder.Entity<Customer>()
.OwnsOne(customer => customer.Address);
Address は単独の主キーを持つ独立エンティティではなく、Customer に所有される値として扱われます。
住所、金額、期間、座標など、値オブジェクト的なものに向いています。
キーなし型
主キーを持たない読み取り専用の結果を扱いたい場合、キーなし型を使えます。
public sealed class InventorySummary
{
public string Make { get; set; } = "";
public int Count { get; set; }
}
設定です。
modelBuilder.Entity<InventorySummary>()
.HasNoKey()
.ToView(null);
生 SQL やビューの結果を受け取る用途に向いています。
List<InventorySummary> summaries = await context
.Set<InventorySummary>()
.FromSqlRaw("""
SELECT Make, COUNT(*) AS Count
FROM Inventory
GROUP BY Make
""")
.ToListAsync();
キーなし型は変更追跡や更新対象には向きません。
集計結果やレポート用の読み取りモデルとして考えると分かりやすいです。
DbContext の寿命に注意する
DbContext は軽量な作業単位として扱います。
長く持ち続けると、追跡対象が増えすぎたり、古いデータを保持したりします。
避けたい例です。
// アプリ全体で 1 つだけ使い回すのは避ける
static AutoLotContext SharedContext = ...;
おすすめは、必要な処理ごとに作って破棄することです。
await using AutoLotContext context = factory.CreateDbContext();
// 1 つの作業を行う
Web アプリでは DI によるスコープ管理がよく使われます。
まとめ
EF Core の基本部品は、DbContext、DbSet<T>、エンティティ、変更追跡です。
DbContext は作業単位、DbSet<T> はエンティティ集合への入口、エンティティはデータベース行に対応するクラスです。
変更追跡を理解すると、追加・更新・削除がどのように SQL へ変換されるのか見えやすくなります。
次回は、LINQ クエリがどのように実行されるか、追跡あり/なしの違い、設計時のアプローチを整理します。