Hatena::ブログ(Diary)

Architect Life このページをアンテナに追加 RSSフィード

2008-07-17

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書いとけよという気がしないでもない。

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

NyaRuRuNyaRuRu 2008/07/17 15:20 >あまりC#とかに詳しくない開発者には結構敬遠されそうな気がする。SQLを組み立てるのに比べると直感的ではないから。

たしかにややこしいですね.
一応 LINQ 的にはこんな感じのコードはいかがでしょう?

まずユーティリティ関数を作っておきます.
static Expression<Func<TSource,TResult>> Expr<TSource, TResult>(Expression<Func<TSource,TResult>> expr){return expr;}
static Expression<Func<TSource, bool>> OrElse<TSource>(this Expression<Func<TSource, bool>> left, Expression<Func<TSource, bool>> right)
{
var param = Expression.Parameter(typeof (TSource), ”p”);
return Expression.Lambda<Func<TSource, bool>>(Expression.OrElse(Expression.Invoke(left, param), Expression.Invoke(right, param)), param);
}

んで,values が文字列のシーケンスを表すとして,

var cond = values.Aggregate(Expr((Document d) => false), (expr, value) => expr.OrElse(d => d.FileName.Contains(value)));
return query.Where(cond);

という感じで注文を満たせるかと.

NyaRuRuNyaRuRu 2008/07/17 15:30 あー,「動的 LINQ (パート 1: LINQ 動的クエリライブラリの使用)」からリンクされている以下のページで同じことやってますね.
http://www.albahari.com/nutshell/predicatebuilder.html
せいぜい違いと言えば,OrElse と Or のどちらを使うかと,連結に foreach を使うか Aggregate を使うかぐらいですな.

coma2ncoma2n 2008/07/17 16:32 なるほどOrElseの右辺、左辺に直接ラムダ式を突っ込めないからどうしようかと思ってたんですが、InvocationExpressionにするとそのラムダ式の結果を返すことになるからOKなんですね。

でも、これで生成した式ツリーを見ると結構階層が深いのに実際に生成されるSQLを見てみると僕の作ったやつと全く同じになってました。なんか複雑。
これは式ツリーの複雑さと実際に生成されるコードの複雑さはあまり関係がないということなんでしょうね。

NyaRuRuNyaRuRu 2008/07/17 17:44 >実際に生成されるSQLを見てみると僕の作ったやつと全く同じになってました。なんか複雑。

これは私も確認してて,結構うれしく思ってました.
最初の Expr((Document d) => false) とか明らかに無駄な式なはずですが,SQL 変換時にちゃんと最適化してくれるんだなぁと.
欲を言えば,Compile メソッドもこれぐらい最適化をやってくれるとうれしいんですけどにゃー.

さかもとさかもと 2008/07/18 21:29 今まさにLINQの勉強始めたところですw
遅すぎなのにブログ徘徊していてこんな高度な話にぶち当たってしまいもう・・・。

coma2ncoma2n 2008/07/18 21:57 なんの、まだまだ序の口ですよw

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証