前回は、EF Core の更新と削除を見ました。
今回は、実務で役立つ問い合わせ系の機能をまとめます。
グローバルフィルター、生 SQL、投影、データベース生成値、関数呼び出し、バッチ処理です。
グローバルフィルター
論理削除を導入すると、通常の検索では削除済みデータを除外したくなります。
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 bool IsDeleted { get; set; }
}
毎回 Where(car => !car.IsDeleted) を書くのは面倒です。
そこで、モデル設定でフィルターを定義します。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasQueryFilter(car => !car.IsDeleted);
}
これで、通常の context.Cars への問い合わせでは、削除済みデータが自動的に除外されます。
List<Car> cars = await context.Cars.ToListAsync();
必要に応じてフィルターを無視できます。
List<Car> allCars = await context.Cars
.IgnoreQueryFilters()
.ToListAsync();
グローバルフィルターは便利ですが、暗黙的に条件が追加されるため、管理画面や集計処理では意識しておく必要があります。
生 SQL と LINQ を組み合わせる
EF Core では、生 SQL を入口にして LINQ を続けることができます。
List<Car> cars = await context.Cars
.FromSqlRaw("""
SELECT Id, Make, Color, PetName, IsDeleted
FROM Inventory
WHERE Make = {0}
""", "Honda")
.Where(car => car.Color == "Blue")
.ToListAsync();
FromSqlRaw() ではパラメーターを使い、値を直接文字列結合しないようにします。
より安全で読みやすい補間版もあります。
string make = "Honda";
List<Car> cars = await context.Cars
.FromSqlInterpolated($"""
SELECT Id, Make, Color, PetName, IsDeleted
FROM Inventory
WHERE Make = {make}
""")
.ToListAsync();
補間文字列で書いていますが、値は SQL パラメーターとして扱われます。
生 SQL を使う場面
生 SQL は、次のような場面で役立ちます。
- 複雑な SQL を直接書きたい
- データベース固有機能を使いたい
- 既存の SQL を活用したい
- パフォーマンス確認済みのクエリを使いたい
- ストアドプロシージャを呼びたい
ただし、EF Core の変更追跡やモデルとの整合性を意識する必要があります。
結果列がエンティティの形と合わない場合は、DTO やキーなし型を検討します。
DTO へ投影する
画面や API では、エンティティ全体ではなく必要な項目だけ返したいことが多いです。
public sealed class CarSummary
{
public int Id { get; init; }
public string DisplayName { get; init; } = "";
public int OrderCount { get; init; }
}
List<CarSummary> summaries = await context.Cars
.AsNoTracking()
.Select(car => new CarSummary
{
Id = car.Id,
DisplayName = car.Color + " " + car.Make + " (" + car.PetName + ")",
OrderCount = car.Orders.Count
})
.ToListAsync();
投影を使うと、必要な列と集計だけを取得できます。
エンティティをそのまま外へ返すより、API の契約も安定します。
データベース生成値
主キーの自動採番、作成日時、更新日時など、データベース側で生成される値があります。
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 DateTime CreatedAt { get; set; }
}
設定例です。
modelBuilder.Entity<Car>()
.Property(car => car.CreatedAt)
.HasDefaultValueSql("SYSUTCDATETIME()");
追加時に CreatedAt を設定しなくても、データベース側で値が入ります。
Car car = new Car
{
Make = "Honda",
Color = "Blue",
PetName = "Vega"
};
context.Cars.Add(car);
await context.SaveChangesAsync();
Console.WriteLine(car.CreatedAt);
保存後、生成された値がエンティティへ反映されます。
データベース関数をマッピングする
データベース関数を C# メソッドとして表現できます。
public static class DbFunctions
{
public static int PetNameLength(string value)
=> throw new NotSupportedException();
}
設定例です。
modelBuilder
.HasDbFunction(typeof(DbFunctions).GetMethod(nameof(DbFunctions.PetNameLength))!)
.HasName("LEN");
問い合わせで使います。
List<Car> cars = await context.Cars
.Where(car => DbFunctions.PetNameLength(car.PetName) > 3)
.ToListAsync();
このメソッドは C# 側で実行するためのものではなく、SQL へ変換するための記述です。
メソッド本体で例外を投げているのは、クライアント側で直接実行される想定ではないためです。
EF.Functions を使う
EF Core には、データベース関数を呼び出すための入口があります。
List<Car> cars = await context.Cars
.Where(car => EF.Functions.Like(car.PetName, "%a%"))
.ToListAsync();
Like は SQL の LIKE に変換されます。
文字列検索を SQL 側で表現したい場合に便利です。
データベースごとに使える関数は異なります。
プロバイダー固有の拡張もあるため、対象DBの機能を意識して使いましょう。
ステートメントのまとめ実行
EF Core は、複数の変更を SaveChanges() でまとめて送ります。
context.Cars.Add(new Car { Make = "Honda", Color = "Blue", PetName = "A" });
context.Cars.Add(new Car { Make = "Ford", Color = "Red", PetName = "B" });
context.Cars.Add(new Car { Make = "Toyota", Color = "Black", PetName = "C" });
await context.SaveChangesAsync();
1 件ごとに SaveChanges() を呼ぶより、まとめて呼ぶ方が効率的です。
避けたい例です。
foreach (Car car in cars)
{
context.Cars.Add(car);
await context.SaveChangesAsync();
}
推奨例です。
context.Cars.AddRange(cars);
await context.SaveChangesAsync();
大量データでは専用の一括投入手段も検討しますが、通常の追加なら SaveChanges() の回数を減らすだけでも効果があります。
まとめ
EF Core には、実務で役立つ便利な機能が多くあります。
グローバルフィルターは論理削除やテナント分離に有効です。
生 SQL は複雑な問い合わせやデータベース固有機能に使えます。
投影は画面や API に必要な形だけ取得するために重要です。
次回は、同時実行制御、接続回復、値変換、シャドウプロパティ、履歴テーブルなど、より設計寄りの機能を見ていきます。