3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EF Coreで「今DB?それともメモリ?」を見失わないための実装者向けまとめ

Posted at

はじめに

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 で迷子になりがちな実装者の助けになれば幸いです。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?