Expression Tree で DSL

データベースを利用したアプリケーション開発で、ユーザの入力した条件に従って抽出する場合など、SQLを動的に作成して発行しなければならない場合があります。そのようなときクエリーオブジェクトを使っていました。クエリーオブジェクトはSQLの文字列作成をラップしたオブジェクトで、例えば次の様な記述になります。

1
2
3
4
5
6
var query = new Query();
query.Select.Add(伝票);
query.Select.Add(new SqlColumn("Coalesce(", 顧客マスタ.住所1Column, ",) || Coalesce(", 顧客マスタ.住所2Column, ",");
query.From = new SqlFrom(伝票).InnerJoin(顧客マスタ).On(伝票.顧客コードColumn, 顧客マスタ.顧客コードColumn);
query.Where.Add(伝票.顧客コードColumn, MatchKind.Equal, ConditionRow.顧客コード);
query.Where.Add(伝票.仮伝票Column, MatchKind.NotEqual, true);

queryはクエリーオブジェクトでSelect,From,Whereなどの各句に適したビルダーをメンバーに持ちます。ConditionRowはユーザが入力した抽出条件を表すオブジェクトです。この方法での問題点は記述量が多いことと、型付きデータセットのメリットを活かせないこと、データベース関数への対応が難しく、その部分だけ文字列の組み立てが必要になるなどです。

解決策をなかなか見つけられずにいましたがExpression TreeによるDSL処理系を開発すれば解決できることがわかりました。

例えば上記のクエリーオブジェクトと同等の記述は次のようになります。

1
2
3
var query = new Query(() => SqlExp.Select(伝票.AllColumns, ((顧客マスタ.住所1 ?? "") + (顧客マスタ.住所2 ?? "")).As(住所))
.From(伝票).InnerJoin(顧客マスタ).On(伝票.顧客コード == 顧客マスタ.顧客コード)
.Where(伝票.顧客コード == ConditionRow.顧客コード && !伝票.仮伝票));

Expression Treeを応用したフレームワークLinq To SQLは標準クエリー演算子と仕様が合わせられています。それに対して当方が取り組んだDSL(SqlExpと命名)はSQL文との互換性を重視しています。ラムダ式を組み合わせてクエリーを表現するLinq To SQL とは違い、1つのラムダ式でクエリーを表現します。また、SELECT文だけでなくINSERT, UPDATE, DELETE文も表現できます。

SqlExp処理系は、与えられたラムダを解釈しExpression TreeをたどってSQLを作成します。

Expression Treeではメソッド名・プロパティ名を取得することもできますし、実行時にコンパイルして値を取得することもできます。伝票顧客マスタ住所1等のテーブル名・カラム名はそのまま文字列に変換し、ユーザが入力したConditionRow.顧客コード等は値を取得してから文字列に変換します。テーブル名・カラム名はスキーマに基づいてコード生成しています。DSL中に表われるSelect,As,From,InnerJoin等のメソッドは実装する必要がなく単なるダミーです。SqlExp処理系でそれらをSQL文字列に変換できればよいのです。また、データベース関数を処理できるように作っています。新たなデータベース関数に対応したい場合には名前と引数リストを合わせたダミーのメソッドを作れば良いだけですので機能を簡単に拡張できます。動的な追加はクエリーオブジェクトと同様に各句のビルダーを使います。ビルダーは文字列ではなくExpression Treeを保持するようにしています。ビルダーをSqlExp上の挿入したい位置に記述することでビルダーの持っている部分式を展開するようにしています。

1
2
3
4
var columns = new SqlColumns();
columns.Add(() => 伝票.伝票番号);
columns.Add(() => 伝票.仮伝票);
var query = new Query(() => SqlExp.Select(columns).From(伝票));

工夫した点としては、比較演算の一方がnullであれば演算自体を消去していることです。抽出条件はユーザが入力しなければ条件に含めないことが多いのですが、そのような場合にも条件分岐を記述する必要がありません。

SqlExp処理系を開発することで下記メリットを得られました。

  • 記述が簡潔になった
  • タイプミスはコンパイル時に気づく
  • データベース関数対応等容易に拡張できるようになった

今のところ開発プロジェクトで使用するDMLをすべて表現できています。

SqlExp処理系開発に際して、Expression Treeについて書いておられるあちらこちらのサイトを参考にさせていただきました。