Expressionを使った動的なOR文の生成

仕事でLINQ to SQLを使ってDBからデータを検索してくるアプリを開発していて、検索する値をスペースで区切った場合はOR検索するという仕様を実装する必要があった。

OR検索自体は以下のように論理演算子で条件をつないでいくだけ。

var result = from d in db.Document
            where d.FileName.Contains("値1") || d.FileName.Contains("値2")
            select d;

こう書けるのは条件の数がわかっているからで、今回の場合は動的に条件が変わるためこの書き方はできない。SQLであれば単に文字列を連結していけばいいだけだけど、LINQの場合はそうはいかない。

なので、こういう時はExpressionを使うことになる(面倒くさいからあまりやりたくなかったけど)。ちなみに、AND条件の場合はWhereメソッドで連結すればいいだけなのでExpressionはいらない。

.NET3.5から追加されたExpressionならラムダ式をオブジェクト構造として組み立てる事ができるので、動的にwhere文に指定するラムダ式を組み立てる事も可能というわけだ。

以下が実際に検索条件から動的にExpressionを組み立てるコード

// wordが検索文字列
var values = word.Split(' ', ' ');
// 基本になるクエリ
var query = from d in db.Document
            select d;

// string.Containsメソッド
var contains = typeof(string).GetMethod("Contains");
// ラムダ式に渡すパラメータ
var paramExpr = Expression.Parameter(typeof(Document), "d");

Expression bodyExpr = null;

foreach(var o in values) {
   if(o.Length == 0) continue;

   if(bodyExpr == null) {
       // d.FileName.Contains("値")のコードと等価
       bodyExpr = Expression.Call(
           Expression.Property(paramExpr, "FileName"), contains, Expression.Constant(o)
       );

   } else {
       // 既に式があればOR演算する
       bodyExpr = Expression.OrElse(
           bodyExpr,
           Expression.Call(
               Expression.Property(paramExpr, "FileName"), contains, Expression.Constant(o)
           )
       );
   }
}
if(values.Length == 0) return query;

return query.Where(
   // 式のまま渡すこと!!Compileしたら駄目!!
   Expression.Lambda<Func<Document, bool>>(bodyExpr, paramExpr)
);

馴れてくれば、こういったExpressionの組み立ても楽にできるようになるんだろうけど、今はまだ頭がついていかない。これを書くだけでも2時間ぐらいかかってしまった。

LINQで動的な条件文を組み立てるにはExpressionを使う必要があります」とかいうと、あまりC#とかに詳しくない開発者には結構敬遠されそうな気がする。SQLを組み立てるのに比べると直感的ではないから。

そういうのを見越したからか、動的LINQライブラリというのもあるらしい。

http://www.scottgu.com/blogposts/dynquery/step2.png

式を文字列で指定して条件を組み立てていくというもの。ならSQL書いとけよという気がしないでもない。

なんにせよ、このへんをもっと簡単にできるライブラリを作る必要がありそう。