ラムダ式でSQLクエリを作ろう

単純なWhere句をいかに外からもスマートに見せるかってのと、なんとなくRailsに憧れて、でもできればコンパイル言語としての固さも残してって事でラムダ式SQLクエリを作ることにした。
まああれですよ、Rails

Table.find(:all, :conditions=>"stasus = 5")

みたいなのがやりたいだけなんです。この書き方ってすごい見やすいですよね。うちのコードだと、検索条件オブジェクトを渡すんですが、それはフラグと値をたっくさん持ったオブジェクトで、SQLを実際に記述してるメソッドが分岐だらけでカオスなんです。。てゆうか、書いてる人はいいよ、後から読む僕は読めないんだよ!
という、自分のコードリーディング能力の低さは棚に上げて突き進んで行きます。

目標形

ズバリこんな感じ

repository.FindEntry(condition => condition.Status == status, condition => condition.PublishDate > "2009-12-31");

まあニュアンスこんな感じって事で。もちろん中ではパラメタイズドクエリにするつもりです。

なにはなくとも引数

要は条件なので返り値がboolであればいいわけですよ、だからPredicate。引数は検索条件に使うパラメータだけ持ったクラスを使う。もちろん検索条件毎にコロコロ変えるのでPredicate。そんでラムダ式コンパイル前のが欲しいので式状態で欲しい。よってExpression>。最後にその条件は複数指定する必要があるので配列受け取り。
そんなこんなでメソッドは

public 返り値 メソッド名<T>(Expression<Predicate<T>>[] conditions) { }

になりました。

返り値

さて、このメソッドの目的は受け取ったラムダ式達をクエリの条件に使えるようにしてやる事です。
よって、形としては

A = B
A > B
A >= B
A < B
A <= B
A <> B

ぐらいを想定している。
where句の中でandで使うとかorで使うとかは使用者が決めればいい話なので、付けない。
よって、"Status = 1"みたいな条件に展開してやって、それを文字列として返してやれば十分だろう。
複数のラムダ式が来る可能性もあるのでstringの配列か。

実はまだ足りない、パラメータだ。でもこの時点でDbParameterオブジェクトなんか作りたくないので、パラメータのキーと値の辞書を一緒に返してやることにする。
よって、一つ目の文字列は"Status = :Status"になって、辞書には["Status", 1]が入ることになる。

それらを一緒に返すためにオブジェクトを新たに作ってやる。ノリ的には無名オブジェクトでもいい気もするが、ここはちゃんとクラスを定義してやる。

/// <summary>
/// クエり条件
/// </summary>
public class QueryCondition
{
    /// <summary>
    /// 条件式<br/>
    /// [形式] カラム名 比較演算子 :カラム名<br/>
    /// 比較演算子はequal, notequal, greaterthan, lessthan, greaterthanorequal, lessthanorequalのみ
    /// <example>
    /// CompanyName &gt; :CompanyName
    /// </example>
    /// </summary>
    public string[] ConditionExpressions { get; set; }
    /// <summary>
    /// パラメータ<br/>
    /// 条件式と同数のパラメータが返されます。<br/>
    /// カラム名がKey、それに対する値がValueに格納されます。
    /// </summary>
    public Dictionary<string, object> Parameters { get; set; }
}

さて、これでメソッドは

public QueryCondition メソッド名<T>(Expression<Predicate<T>>[] conditions) {  }

となったわけだ。

Tの実装は?

Tは検索条件であるが、実はあまり意味のないクラスになる。それっていいのか?という疑問はすごいあるがとりあえず忘れる。
ラムダ式から名前を取る便宜上パラメータはテーブルのカラム名と全く同名とする。
※後でそうしなくてもいいとわかったが、めんどいからそのまま行きます。

メソッドの実装

返り値の内容が決まったところで一気に実装する。

public static QueryCondition Parse<T>(Expression<Predicate<T>>[] conditions)
{
    var tmpParameters = new Dictionary<string, object>();
    var whereExps = 
        Array.ConvertAll<Expression<Predicate<T>>, string>(conditions, condition =>
        {
            var conditionMember = (((MemberExpression)((BinaryExpression)condition.Body).Left)).Member;
            var memberRight = (((BinaryExpression)condition.Body).Right);
            // リテラルならConstantExpression、それ以外ならMemberExpressionから深追いしていく。
            var conditionValue = memberRight is ConstantExpression ? ((ConstantExpression)memberRight).Value
                                                                   : GetMemberValue(memberRight as MemberExpression);
            
            string compareSymbol = string.Empty;
            switch (condition.Body.NodeType)
            {
                case ExpressionType.Equal:
                    compareSymbol = "=";
                    break;
                case ExpressionType.GreaterThan:
                    compareSymbol = ">";
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    compareSymbol = ">=";
                    break;
                case ExpressionType.LessThan:
                    compareSymbol = "<";
                    break;
                case ExpressionType.LessThanOrEqual:
                    compareSymbol = "<=";
                    break;
                case ExpressionType.NotEqual:
                    compareSymbol = "<>";
                    break;
                default:
                    throw new ArgumentException();
            }
            tmpParameters.Add(conditionMember.Name, conditionValue);
            return string.Format(" {0} {1} :{2} ",
                conditionMember.Name, compareSymbol, conditionMember.Name);
        });

    return new QueryCondition { ConditionExpressions = whereExps, Parameters = tmpParameters };
}

private static Object GetMemberValue(MemberExpression MemberExpression, PropertyInfo PropertyInfo)
{
    return PropertyInfo.GetValue(GetMemberValue(MemberExpression), null);
}

private static Object GetMemberValue(MemberExpression MemberExpression)
{
    var MemberExpression_Expression = MemberExpression.Expression;
    if (MemberExpression_Expression.NodeType == ExpressionType.MemberAccess)
    {
        return GetMemberValue(MemberExpression_Expression as MemberExpression, MemberExpression.Member as PropertyInfo);
    }
    else
    {
        var Constant = MemberExpression_Expression as ConstantExpression;
        var FieldInfo = MemberExpression.Member as FieldInfo;
        return FieldInfo.GetValue(Constant.Value);
    }
}

なんかキャストすごい、、とかは禁句です。結構無理やりやってる感がいっぱいです。でも気にしない、実務で使うわけじゃないから!

はまったとこ

渡すラムダ式の比較値がリテラルの場合("conditon.Status == 1")はすぐにできたのだが、外部変数を持ち込む場合("condition.Status = status")が最初よくわからなかった。とにかくデバッグのウォッチ式で突き進んで、最後はリフレクションでアクセスするという事になったが他にいい方法はなかったのだろうか。。

さっそく使う

仮にさっきのメソッドがQueryConditionParserというクラスのメソッドだとして

public class ConditionElement { 
  public int ID;
}
QueryConditionParser.Parse(new Expression<Predicate<ConditionElement>> { condition => condition.ID == 1 })
# "ID = :ID"
# ["ID", 1]

後はこのメソッドを使うメソッドを定義して、それの引数を(param Expression>[] condtions)とかにするといい感じになりますね。

まとめ

何かが違う。あ、そうかRubyを使えばいいんだよ!終わり。