C# Tips | コレクション・LINQ:LINQ式キャッシュ

C# C#
スポンサーリンク

はじめに:「LINQ式キャッシュ」は“同じクエリを何度も組み立てないための知恵”

LINQ を使っていると、こういうコードが増えがちです。

var q1 = users.Where(x => x.IsActive && x.Age >= 20);
var q2 = users.Where(x => x.IsActive && x.Age >= 20 && x.Age <= 30);
var q3 = users.Where(x => x.IsActive && x.Age >= 20 && x.Name.Contains("田"));
C#

「毎回ほぼ同じ条件を書いている」「ちょっとずつ違うバリエーションが増えていく」
これが積み重なると、保守がつらくなります。

ここで出てくる考え方が LINQ式キャッシュ です。
ざっくり言うと、

「よく使う LINQ の“式(条件や並び替え)”を、再利用できる形でどこかに貯めておく」

という発想です。

ここでは、初心者向けに

LINQ の「式」と「結果」の違い
Expression<Func<T, bool>> を使った条件のキャッシュ
条件を組み合わせるためのユーティリティ
実務での「よく使う絞り込み・並び替え」を式としてキャッシュするイメージ

を、例題付きでかみ砕いて説明します。


「式」と「結果」を分けて考える

LINQ は「式」を組み立ててから「実行」している

まず、LINQ には大きく 2 種類あります。

配列や List に対する LINQ(LINQ to Objects)
DB(EF など)に対する LINQ(LINQ to Entities など)

どちらも、見た目は同じように書けますが、
内部的には「式(Expression)」として組み立ててから、
必要になったタイミングで実行されています。

例えば、こういうコード。

var query = users.Where(x => x.IsActive && x.Age >= 20);
// ここではまだ実行されていない(式だけ)

var list = query.ToList(); // ここで初めて実行される
C#

ここでの重要ポイントは、「Where に渡している x => x.IsActive && x.Age >= 20 という“式”を、再利用できる形で持っておくと便利」ということです。
これが「LINQ式キャッシュ」の入り口です。


条件式を Expression としてキャッシュする

Func<T, bool> と Expression<Func<T, bool>> の違い

まず、よく見るこの形。

Func<User, bool> predicate = x => x.IsActive && x.Age >= 20;
C#

これは「C# のコードとしての関数」です。
配列や List に対してはそのまま使えますが、
DB に対する LINQ(EF など)では「式ツリー」が必要になります。

using System.Linq.Expressions;

Expression<Func<User, bool>> expr = x => x.IsActive && x.Age >= 20;
C#

Expression<Func<...>> は、「式そのもの」をオブジェクトとして持てる型です。
これをキャッシュしておくと、

何度も同じ Where 条件を使い回せる
別の条件と組み合わせて、新しい式を作れる

といったことができるようになります。

ここでの重要ポイントは、「“式をキャッシュしたい”ときは、Func ではなく Expression<Func<…>> を使う」ということです。


例1:よく使うフィルタ条件を式としてキャッシュする

「アクティブユーザー」の条件を 1 箇所にまとめる

User クラスを用意します。

public class User
{
    public bool IsActive { get; set; }
    public int Age { get; set; }
    public string Name { get; set; } = "";
}
C#

「アクティブユーザー」の条件を、あちこちでこう書いているとします。

var activeUsers = users.Where(x => x.IsActive);
var activeAndAdult = users.Where(x => x.IsActive && x.Age >= 20);
C#

これを「式としてキャッシュ」してみます。

using System.Linq.Expressions;

public static class UserPredicates
{
    public static readonly Expression<Func<User, bool>> IsActive
        = x => x.IsActive;

    public static readonly Expression<Func<User, bool>> IsAdult
        = x => x.Age >= 20;
}
C#

使う側はこう書けます。

var activeUsers = users.Where(UserPredicates.IsActive);
var activeAndAdult = users.Where(UserPredicates.IsActive)
                          .Where(UserPredicates.IsAdult);
C#

DB に対する LINQ(EF など)でも、そのまま使えます。

ここでの重要ポイントは、「“よく使う条件”を Expression として static に持っておくと、コピペを減らせて、変更も 1 箇所で済む」ということです。


条件式を組み合わせるユーティリティ

Expression を AND / OR でつなげたい

「アクティブかつ成人」のように、式同士を組み合わせたい場面が出てきます。
単純に UserPredicates.IsActive && UserPredicates.IsAdult とは書けません。

そこで、Expression を合成するユーティリティを用意します。
細かい仕組みは一旦置いて、使い方のイメージから見てください。

public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var param = Expression.Parameter(typeof(T), "x");

        var body = Expression.AndAlso(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param));

        return Expression.Lambda<Func<T, bool>>(body, param);
    }

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var param = Expression.Parameter(typeof(T), "x");

        var body = Expression.OrElse(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param));

        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}
C#

これを使うと、こう書けます。

var activeAndAdultExpr =
    UserPredicates.IsActive.And(UserPredicates.IsAdult);

var result = users.Where(activeAndAdultExpr);
C#

ここでの重要ポイントは、「Expression を“部品”として持っておき、AND / OR で組み合わせて新しい式を作れる」ということです。
これが「LINQ式キャッシュ」の一番おいしいところです。


例2:検索画面の条件を動的に組み立てる

「入力された項目だけ条件にする」典型パターン

検索画面で、こういう項目があるとします。

アクティブのみ(bool? isActiveFilter)
最小年齢(int? minAge)
名前に含まれる文字列(string? nameContains)

入力されたものだけ条件にしたい。
これを Expression で組み立ててみます。

Expression<Func<User, bool>> BuildUserFilter(
    bool? isActiveFilter,
    int? minAge,
    string? nameContains)
{
    Expression<Func<User, bool>> predicate = x => true; // 何もしない初期値

    if (isActiveFilter == true)
    {
        predicate = predicate.And(UserPredicates.IsActive);
    }

    if (minAge.HasValue)
    {
        Expression<Func<User, bool>> ageExpr = x => x.Age >= minAge.Value;
        predicate = predicate.And(ageExpr);
    }

    if (!string.IsNullOrEmpty(nameContains))
    {
        Expression<Func<User, bool>> nameExpr = x => x.Name.Contains(nameContains);
        predicate = predicate.And(nameExpr);
    }

    return predicate;
}
C#

使う側はこうです。

var filter = BuildUserFilter(true, 20, "田");
var result = users.Where(filter).ToList();
C#

ここでの重要ポイントは、「“検索条件を Expression として組み立ててキャッシュしておくと、DB に対する LINQ でもそのまま使える”」ということです。
条件の組み立てロジックと、実際のクエリ実行をきれいに分離できます。


「式そのもの」をキャッシュする意味

同じクエリ構造を何度も使うときに効いてくる

LINQ to Entities(EF など)では、
「式ツリーを解析して SQL に変換する」コストがそれなりにあります。

同じような Where / Select / OrderBy を何度も書いていると、
毎回「式の構造を解析→SQL 生成」が走ります。

式を 1 回組み立てて static に持っておき、
パラメータだけ変えて使い回すことで、

クエリ構造の再利用
条件の一元管理
バグの混入ポイントの削減

といったメリットが出てきます。

例えば、「ユーザー一覧の標準クエリ」を式として持っておくイメージです。

public static class UserQueries
{
    public static readonly Expression<Func<IQueryable<User>, IQueryable<User>>> ActiveOrdered
        = q => q.Where(x => x.IsActive)
                .OrderBy(x => x.Name);
}
C#

使う側はこうです。

var query = UserQueries.ActiveOrdered.Compile()(db.Users);
var list = query.ToList();
C#

ここでの重要ポイントは、「“クエリの形”を式としてキャッシュしておくと、“どの画面も同じロジックでデータを取っている”ことを保証しやすい」ということです。
バラバラに LINQ を書くより、設計として強くなります。


実務での「LINQ式キャッシュ」の使いどころ

よく使うフィルタ・並び順・ページング

例えば、ユーザー一覧の標準クエリを考えます。

アクティブのみ
名前昇順
ページング(ページ番号と件数)

これを毎回バラバラに書くのではなく、「式+メソッド」としてまとめておきます。

public static class UserQueryExtensions
{
    public static IQueryable<User> ApplyDefaultFilter(this IQueryable<User> q)
        => q.Where(x => x.IsActive);

    public static IOrderedQueryable<User> ApplyDefaultOrder(this IQueryable<User> q)
        => q.OrderBy(x => x.Name);

    public static IQueryable<User> ApplyPaging(this IQueryable<User> q, int page, int pageSize)
        => q.Skip((page - 1) * pageSize).Take(pageSize);
}
C#

使う側はこうです。

var query = db.Users
    .ApplyDefaultFilter()
    .ApplyDefaultOrder()
    .ApplyPaging(page, pageSize);

var list = query.ToList();
C#

ここでは Expression を直接触っていませんが、
「クエリの形を再利用する」という意味で、
“LINQ式キャッシュ”と同じ発想です。

ここでの重要ポイントは、「“よく使うクエリの形”を、拡張メソッドや Expression としてどこかにまとめておくと、業務コードが一気に読みやすくなる」ということです。


まとめ:「LINQ式キャッシュ」は“LINQ をコピペしないための設計テクニック”

LINQ式キャッシュの本質は、

Where や OrderBy の「式そのもの」を
Expression や拡張メソッドとして再利用可能な形にしておき、
同じクエリ構造を何度も書かないようにする

ことです。

押さえておきたいポイントを整理すると、

「式そのもの」を扱いたいときは Func ではなく Expression<Func<…>> を使う
よく使う条件は static な Expression としてキャッシュしておくと、コピペが減る
Expression を AND / OR で合成するユーティリティ(PredicateBuilder 的なもの)があると、動的検索が書きやすい
「クエリの形」を拡張メソッドや Expression としてまとめておくと、どの画面も同じロジックでデータを取れる

ここまで腹落ちしていれば、
「その場その場で LINQ を書き散らす」段階から抜けて、
“LINQ の式を設計して再利用する”という一段上の書き方に踏み出せます。

タイトルとURLをコピーしました