2011年7月2日土曜日

LambdaExpressionで動的な検索条件を作る

最近の仕事で固定の検索条件ではなく動的に検索条件を作りたいという仕様があり、それもあってExpression周りをいじくりまわしていて楽しかったので解説する。

今回のサンプルコードはこちらからダウンロードできる。


Linqを使用する場合に動的に検索条件を作るためにはExpression Treeと戯れる必要がある。Linq To SqlであればDynamic Linqという文字列を解析してExpression Treeに変換してくれる拡張関数群が公開されているので、文字列をいじくるのを苦にしなければかなり簡単に動的な検索条件を作成することが可能だ。しかし、Linq To Entityだとそうはいかないのでちょっと込み入ったことをしなくてはならない。

動的検索条件と言っても、すべての検索条件がAnd条件であるならば、単純にWhereメソッドを下記のように順々に呼び出していけばよいだろう。

if ( 条件1 )
  query = query.Where(c => c.Name == "四郎" );

if ( 条件2)
  query = query.Where(c => c.Age > 15 );

※今回の解説の仕様として、等号などのオペレーターや対象プロパティは事前に決まっていてどの条件を組み合わせるかのみ動的に設定できる、という設定とする。オペレーターやプロパティもすべてを動的に作る場合にはParameterExpressionなどを使ってさらに深くExpression Treeをいじる必要があるのでその解説は次回以降行う、かもしれない。

しかし、Or検索が間に入ってくると話が変わってくる。前述の方法ではWhereメソッド間はAnd条件になるので動的なOr検索を行うことはできない。というわけで、LambdaExpressionの登場だ。こいつを上手に使ってやると下記のような条件を簡単に動的に組み立てることができる。

query.Where(c => (c.Name.Contains("白") || c.Bushous.Any(b => b.Age > 36)) && c.Name.EndsWith("城"));

今回使用するモデルは下記。DBの作成はEF Code First4.1に全部任せている。
public class Bushou
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int CastleId { get; set; }
    public Castle Castle { get; set; }
}

public class Castle
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual IList<Bushou> Bushous { get; set; }
}
    
public class Ikusa : DbContext
{
    public DbSet<Bushou> Bushous { get; set; }
    public DbSet<Castle> Castles { get; set; }
}

前述の各条件を表すLambdaExpressionは下記になる。

Expression<Func<Castle, bool>> condition1 = c => c.Name.Contains("白");
Expression<Func<Castle, bool>> condition2 = c => c.Bushous.Any(b => b.Age > 36);
Expression<Func<Castle, bool>> condition3 = c => c.Name.EndsWith("城");

このままでは各LambdaExpressionはばらばらなので一つにまとめる必要がある。その際に注意が必要なのだが、LambdaExpressionを統合するには同一の引数を使用するように変更してやらなければならない。上記の定義を見てもらえば分かると思うけれど、各LambdaExpressionにはcという引数が定義されている。Expression Tree的にはcondition1のcとcondition2、condition3のcはまったくの別物なので、こいつらを一つにまとめる必要がある。

で、そんな面倒なことを行う際に便利なクラスがC#4.0から提供されている。それがExpressionVisitorクラスだ。このクラスの使いどころは次のような場合だ。Expressionには多種多様なタイプが用意されている。go toステートメントを表すGoToExpression、ループを表すLoopExpression、メンバーアクセスを表すMemberExpressionなどなど数十種類に及ぶ。で、今回は引数をいじくる必要があるのでParameterExpressionが対象になる。しかしExpression Treeを順々にたどっていって任意のExpressionを探すのは骨が折れる。なのでそんなときはExpressionVisitorクラスを使用する。

使い方はExpressionVisitorクラスを継承し、自分が気になるタイプのExpressionを引数としているメソッドをオーバーライドするだけだ。今回はParameterExpressionをいじくるのでVisitParameterがオーバーライドするメソッドになる。

class ParameterVisitor : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> _fromParams, _toParams;
    public ParameterVisitor(ReadOnlyCollection<ParameterExpression> from, ReadOnlyCollection<ParameterExpression> to)
    {
        _fromParams = from;
        _toParams = to;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        for (var i = 0; i < _fromParams.Count; i++)
        {
            if (node == _fromParams[i])
                return _toParams[i];
        }
        return node;
    }
}

ParameterVisitorクラスは変更元の引数コレクションと変更先の引数コレクションを保持し、ParameterExpressionが見つかるたびに、その見つかった引数(上記で言うところのnode)が変更元の引数と同一かをチェックし、同一であれば、変更先の引数を返却している。変更先の引数を返却することで変更元のExpression Treeは差し替えられている。つまり、変更元の引数の数と、変更先の引数の数が異なる場合はアボンするということなので注意が必要だ。ParameterVisitorクラスのコンストラクタにそれようのチェックコードを入れるのも良いだろう。

実際にParameterVisitorクラスの使用方法は下記になる。

class Helper
{
    public static Expression<Func<T, bool>> AndAlso<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        var newExp = new ParameterVisitor(right.Parameters, left.Parameters).Visit(right.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body, newExp), left.Parameters);
    }

    public static Expression<Func<T, bool>> OrElse<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        var newExp = new ParameterVisitor(right.Parameters, left.Parameters).Visit(right.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body, newExp), left.Parameters);
    }
}

各メソッドともに、まずはVisitメソッドを呼び出し、二個目の引数のLambdaExpressionの引数を一個目の引数のLambdaExpressionの引数で差し替えている。その後にAndAlso、またはOrElseを呼び出しExpressionの統合を行っている。

Helperクラスの使い方は下記になる。

var condition = Helper.AndAlso(Helper.OrElse(condition1, condition2), condition3);

そうしてできたLambdaExpressionをWhereメソッドに渡してやれば下記と同等の記述になる。

query.Where(c => (c.Name.Contains("白") || c.Bushous.Any(b => b.Age > 36)) && c.Name.EndsWith("城"));

query = query.Where(condition);

かなり冗長になるけれど、上記の方法以外にも下記のようにWhereメソッドを呼び出すこともできる。

var resultExp = Expression.Call(typeof(Queryable),
                                "Where",
                                new Type[] { typeof(Castle) },
                                query.Expression,
                                Expression.Quote(condition));
query = query.Provider.CreateQuery<Castle>(resultExp);

今回の例では下のほうの呼び出し方をする必要はまったくないけれど、オペレーターやプロパティも含めてすべてを動的で行う場合などで、Anyやら何やらを動的に呼び出す必要がある場合は、最後に解説したような呼び出し方をしなければならない。

1 件のコメント:

  1. はじめまして
    この度 CodeManual というサイトを開設しました。

    内容と致しましては、
    自身のブログにメモとして書いたコードを
    他の人達と共有しようというものです。

    もしよろしければご参加ください。

    HP:http://codemanual.info/

    返信削除