前回は、EF Core の基本部品を見ました。
今回は、クエリ実行と追跡を扱います。
EF Core では LINQ で問い合わせを書けますが、その LINQ がいつ実行されるのか、どこまで SQL に変換されるのかを理解しておくことが重要です。
クエリはすぐには実行されない
次のコードを見てください。
IQueryable<Car> query = context.Cars
.Where(car => car.Make == "Honda")
.OrderBy(car => car.PetName);
この時点では、まだデータベースへ問い合わせは送られていません。
IQueryable<T> は、問い合わせの形を表しているだけです。
実際に実行されるのは、結果を必要としたタイミングです。
List<Car> cars = await query.ToListAsync();
ToListAsync()、FirstOrDefaultAsync()、CountAsync() などを呼ぶと、SQL が生成されて実行されます。
すべてのレコードを取得する
List<Car> cars = await context.Cars
.OrderBy(car => car.Id)
.ToListAsync();
これは、Inventory テーブルから全件を取得します。
件数が少ないテーブルなら問題ありませんが、大きなテーブルでは危険です。
実務では、条件、並べ替え、件数制限を意識しましょう。
List<Car> cars = await context.Cars
.Where(car => car.Make == "Honda")
.OrderBy(car => car.Id)
.Take(50)
.ToListAsync();
条件で絞り込む
string make = "Honda";
List<Car> cars = await context.Cars
.Where(car => car.Make == make)
.ToListAsync();
EF Core は、変数 make を SQL パラメーターとして扱います。
文字列連結で SQL を組み立てるより安全です。
部分一致も書けます。
List<Car> cars = await context.Cars
.Where(car => car.PetName.Contains("a"))
.ToListAsync();
ただし、文字列操作がどのような SQL に変換されるかは、プロバイダーや照合順序にも影響されます。
並べ替えとページング
int page = 2;
int pageSize = 10;
List<Car> cars = await context.Cars
.OrderBy(car => car.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
ページングでは、必ず安定した並べ替えを指定しましょう。
Skip と Take だけでは、データベースがどの順番で返すか保証されません。
1 件だけ取得する
Car? car = await context.Cars
.FirstOrDefaultAsync(car => car.Id == 1);
条件に一致するものがなければ null になります。
必ず 1 件ある前提なら SingleAsync() や SingleOrDefaultAsync() もありますが、件数チェックの意味が変わります。
Car car = await context.Cars.SingleAsync(car => car.Id == 1);
SingleAsync() は、0 件でも 2 件以上でも例外になります。
主キー検索なら FindAsync() も使えます。
Car? car = await context.Cars.FindAsync(1);
集計する
int count = await context.Cars.CountAsync();
int hondaCount = await context.Cars
.CountAsync(car => car.Make == "Honda");
最大値や最小値も取得できます。
int maxId = await context.Cars.MaxAsync(car => car.Id);
集計は、通常データベース側で実行されます。
全件をアプリケーションへ読み込んでから数えるより効率的です。
Any と All
存在確認には AnyAsync() が便利です。
bool exists = await context.Cars
.AnyAsync(car => car.Make == "Honda");
すべての行が条件を満たすか確認するには AllAsync() を使います。
bool allNamed = await context.Cars
.AllAsync(car => car.PetName != "");
存在確認だけなら、CountAsync() > 0 より AnyAsync() の方が意図が明確です。
サーバー評価とクライアント評価
EF Core の LINQ は、可能な限り SQL に変換され、データベース側で実行されます。
List<Car> cars = await context.Cars
.Where(car => car.Make == "Honda")
.ToListAsync();
この条件は SQL に変換できます。
一方、C# の任意のメソッドがすべて SQL に変換できるわけではありません。
static bool IsSpecialName(string value)
{
return value.StartsWith("A") && value.Length > 3;
}
このような独自メソッドを Where の中で使うと、SQL に変換できず問題になることがあります。
// 避けたい例
List<Car> cars = await context.Cars
.Where(car => IsSpecialName(car.PetName))
.ToListAsync();
データベースで実行できる条件と、アプリ側でしか実行できない処理を分けて考えましょう。
List<Car> candidates = await context.Cars
.Where(car => car.PetName.StartsWith("A"))
.ToListAsync();
List<Car> result = candidates
.Where(car => IsSpecialName(car.PetName))
.ToList();
先にデータベース側で絞ってから、必要な分だけアプリ側で処理するのが安全です。
追跡ありクエリ
通常、EF Core は取得したエンティティを追跡します。
Car? car = await context.Cars.FirstOrDefaultAsync(car => car.Id == 1);
if (car != null)
{
car.PetName = "Updated";
await context.SaveChangesAsync();
}
このように、取得後に変更して保存する場合は追跡が役立ちます。
追跡なしクエリ
読み取り専用なら、追跡なしにできます。
List<Car> cars = await context.Cars
.AsNoTracking()
.Where(car => car.Make == "Honda")
.ToListAsync();
追跡なしにすると、変更検知の対象にならないため、読み取り処理では軽くなります。
一覧表示、検索結果、レポートなど、変更しないデータには AsNoTracking() を検討しましょう。
投影で必要な列だけ取得する
エンティティ全体が不要なら、必要な形へ投影します。
var cars = await context.Cars
.AsNoTracking()
.Select(car => new
{
car.Id,
DisplayName = car.Color + " " + car.Make,
car.PetName
})
.ToListAsync();
DTO を使うこともできます。
public sealed class CarListItem
{
public int Id { get; init; }
public string DisplayName { get; init; } = "";
public string PetName { get; init; } = "";
}
List<CarListItem> cars = await context.Cars
.AsNoTracking()
.Select(car => new CarListItem
{
Id = car.Id,
DisplayName = car.Color + " " + car.Make,
PetName = car.PetName
})
.ToListAsync();
画面表示や API 応答では、エンティティをそのまま返すより DTO へ投影する方が扱いやすいことが多いです。
モデル主導とデータベース主導
EF Core の開発スタイルには、大きく 2 つの考え方があります。
モデルからデータベースを作る
C# のエンティティと DbContext を先に定義し、マイグレーションでデータベースを作ります。
新規開発で使いやすい方法です。
既存データベースからモデルを作る
既存のデータベーススキーマを読み取り、DbContext とエンティティを生成します。
既存システムや既存DBへ接続する場合に便利です。
どちらが正しいというより、プロジェクトの出発点がどこにあるかで選びます。
まとめ
EF Core のクエリは、LINQ で表現され、必要になったタイミングで SQL として実行されます。
どこまでデータベース側で処理されるのか、どこからアプリ側で処理されるのかを意識することが重要です。
読み取り専用では AsNoTracking()、画面表示では投影、更新する場合は追跡あり、と使い分けると無駄が減ります。
次回は、EF Core のコマンドラインツールとマイグレーションを使い、モデル変更をデータベースへ反映する方法を見ます。