はじめに
EF Core を使っていて、
今この LINQ は DB に対して組み立てている最中なのか
それとも もうメモリに展開された後なのか
が分からなくなることはありませんか?
この記事では 設計論は一旦置いて、
実装者視点での判断基準・安全な書き方・アンチパターンをまとめます。
※この記事は
「EF Coreは一通り触れるが、パフォーマンスや例外で痛い目を見始めた人」
向けです。
結論:EF Core は「結果が必要になった瞬間」に DB を叩く
EF Core の LINQ は、 書いた瞬間に実行されているわけではありません。
値が必要になった瞬間 = SQL が発行される
これを軸に考えると、一気に見通しが良くなります。
DB にアクセスする(=メモリに落ちてくる)代表的なメソッド
以下を見たら 「ここで DB に行く」 と判断してOKです。
ToList()
ToArray()
First()
FirstOrDefault()
Single()
SingleOrDefault()
Count()
Any()
Max()
Min()
Sum()
foreach
これらはすべて 結果(値)を要求する操作です。
まだ DB を叩いていない状態とは?
var query = db.Users
.Where(u => u.IsActive)
.OrderBy(u => u.CreatedAt);
- 型は IQueryable
- SQL はまだ発行されていない
- これは クエリの設計図
実行タイミングのイメージ
DbSet<User> (IQueryable)
↓ Where
↓ Select
↓ OrderBy
--------------------
↓ ToList() ← ★ここで SQL 発行
List<User>
IQueryable → Where → Select → OrderBy → ToList() → List<T>
ToList() が 境界線です。
よくあるアンチパターンと安全な書き換え
アンチ①:Where の中で自作メソッド
// NG
db.Users.Where(u => IsAdult(u)).ToList();
EF Core は メソッドの中身を読めません。
安全な書き換え
db.Users.Where(u => u.Age >= 20).ToList();
生成される SQL(イメージ)
SELECT * FROM Users WHERE Age >= 20
アンチ②:拡張メソッドをそのまま Where に使う
public static bool IsAdult(this User u)
{
return u.Age >= 20;
}
// NG
db.Users.Where(u => u.IsAdult());
安全な書き換え(Expression を返す)
public static Expression<Func<User, bool>> IsAdult()
{
return u => u.Age >= 20;
}
// OK
db.Users.Where(UserPredicates.IsAdult());
👉 EF Core が理解できるのは Expression
アンチ③:AsEnumerable を途中に挟む
// NG
db.Users
.AsEnumerable()
.Where(u => u.IsActive)
.ToList();
- この時点で 全件取得確定
- フィルタは C# 側
アンチ④:foreach の中で DB クエリ(N+1)
foreach (var user in users)
{
user.HasOrders = db.Orders.Any(o => o.UserId == user.Id);
}
安全な書き換え
var users = db.Users
.Select(u => new
{
User = u,
HasOrders = db.Orders.Any(o => o.UserId == u.Id)
})
.ToList();
SQL(イメージ)
SELECT u.*,
EXISTS (
SELECT 1 FROM Orders o WHERE o.UserId = u.Id
) AS HasOrders
FROM Users u
アンチ⑤:Select の中で .NET 処理を書きすぎる
// NG
.Select(u => new UserDto
{
Label = $"{u.Id}:{u.Name}"
});
安全な分離
var rows = db.Users
.Select(u => new { u.Id, u.Name })
.ToList();
var result = rows.Select(u => new UserDto
{
Label = $"{u.Id}:{u.Name}"
});
安全な拡張メソッドの書き方(実装者向け)
Where 用
public static IQueryable<User> Active(this IQueryable<User> query)
{
return query.Where(u => u.IsActive);
}
db.Users.Active().ToList();
SQL
SELECT * FROM Users WHERE IsActive = 1
Select(射影)用
public static IQueryable<UserRow> ToRow(this IQueryable<User> query)
{
return query.Select(u => new UserRow
{
Id = u.Id,
Name = u.Name,
CreatedAt = u.CreatedAt
});
}
実装者向けセルフチェックリスト
- 今この変数、型は IQueryable? List?
- AsEnumerable() が混ざっていないか
- foreach の中で db. を呼んでいないか
- Select の中でメソッドを呼んでいないか
- 「これ SQL で表現できる?」と自問したか
最低限これだけ覚えればOK
- IQueryable は「まだDB」
- ToList / Any / Count / foreach を見たら「DBに行った」
- AsEnumerable() は DB とメモリの境界線
- EF Core が読めるのは Expression
まとめ
EF Core の LINQ は C# を書いているようで、実質 SQL を組み立てている
- DB かメモリかを意識する
- 境界は ToList()
- EF が読めるのは Expression
これだけで、 「動くけど遅い」「本番で落ちる」コードは激減します。
この記事が、 EF Core で迷子になりがちな実装者の助けになれば幸いです。