Somaの式コメントでオリジナルの関数を使う

このエントリは、Soma 1.2.0.0を利用する際のTipsです。

はじめに

Somaでは、SQLを動的に組み立てるためにSQLコメント(式コメント)内で利用できる関数があらかじめ組み込まれていて、たとえば次のように使えます。

select * from Employee where 
/*%if not (isNullOrEmpty employeeName) */
    EmployeeName = /* employeeName */'smith'
/*%end */

この例では isNullOrEmpty があらかじめ組み込まれた関数で、引数がnullだったり空文字だった場合にtrueを返します。

以降では、isNullOrEmpty のような関数をアプリケーションで定義し利用する方法を紹介します。

パラメータで渡す方法

一番簡単なのは、パラメータで渡す方法です。次の例では、問い合わせ条件がnullだったりオプション型のNoneだったりした場合に"UNKNOWN"という文字列に置換するadjustという関数を作成してquery関数のパラメータに渡しています。

open System
open Soma.Core

type Condition =
{ Name : string option
Salary : decimal option }

[<EntryPoint>]
let main args =

let adjust (obj:obj) =
let unknown = "UNKNOWN"
match obj with
| :? option<string> as x ->
if
x.IsSome then x.Value else unknown
| x ->
if
x <> null then x.ToString() else unknown

let config =
{ new MsSqlConfig() with
member
this.ConnectionString = "Data Source=.;Initial Catalog=tempdb;Integrated Security=True" }

let employee =
Db.query<dynamic>
config
"select * from Employee where EmployeeName = /* adjust c.Name */'test' and Salary > /* c.Salary */0"
["c" @= { Name = None; Salary = Some 1000M }; "adjust" @= adjust]

printfn "%A" employee

Console.ReadKey() |> ignore
0

この例を動かすと次のSQLがログ出力されます。

LOG : select * from Employee where EmployeeName = N'UNKNOWN' and Salary > 1000

Dialectに登録する方法

上の方法は簡単ですが、毎回パラメータで渡すのは面倒です。
といわけで次に説明するのは、一度登録しておけば使える方法です。次の例では、MsSqlDialectをオブジェクト式で生成する際にadjust関数を登録しています。利用するときは登録した名前で参照して使います。先ほどの例と違ってquery関数のパラメータでは渡していないことに注目してください。

open System
open System.Collections.Generic
open Soma.Core

type Condition =
{ Name : string option
Salary : decimal option }

[<EntryPoint>]
let main args =

let adjust (obj:obj) =
let unknown = "UNKNOWN"
match obj with
| :? option<string> as x ->
if
x.IsSome then x.Value else unknown
| x ->
if
x <> null then x.ToString() else unknown

let dialect =
{ new MsSqlDialect() with
member
this.RootExprCtxt =
let exprCtxt = new Dictionary<string, obj * Type>(base.RootExprCtxt) :> IDictionary<string, obj * Type>
exprCtxt.["adjust"] <- (box adjust, adjust.GetType())
exprCtxt } :> IDialect

let config =
{ new MsSqlConfig() with
member
this.ConnectionString = "Data Source=.;Initial Catalog=tempdb;Integrated Security=True"
member this.Dialect = dialect }

let employee =
Db.query<dynamic>
config
"select * from Employee where EmployeeName = /* adjust c.Name */'test' and Salary > /* c.Salary */0"
["c" @= { Name = None; Salary = Some 1000M }]

printfn "%A" employee

Console.ReadKey() |> ignore
0

この例を動かすと先ほどの例と同じく次のSQLがログ出力されます。

LOG : select * from Employee where EmployeeName = N'UNKNOWN' and Salary > 1000

既存の組み込み関数に別名をつける

Dialectに登録する方法を使うと、既存の組み込み関数に別名をつけたりすることも簡単です。たとえば、先ほどの例のMsSqlDialectのオブジェクト式のところを次のように変更し、「isNullOrEmpty」の別名として「hoge」を登録します。

let dialect = 
{ new MsSqlDialect() with
member
this.RootExprCtxt =
let origCtxt = base.RootExprCtxt
let exprCtxt = new Dictionary<string, obj * Type>(origCtxt) :> IDictionary<string, obj * Type>
exprCtxt.["adjust"] <- (box adjust, adjust.GetType())
exprCtxt.["hoge"] <- origCtxt.["isNullOrEmpty"]
exprCtxt } :> IDialect
この設定をすれば、一番最初の組み込み関数を利用したSQLの例は次のように書けることになります。つまり、「isNullOrEmpty」の代わりに「hoge」と書けます。

select * from Employee where 
/*%if not (hoge employeeName) */
    EmployeeName = /* employeeName */'smith'
/*%end */

おわりに

上の例はF#でしたが、実はC#でも同じようなことができます。C#の場合はFunc型の関数を登録すればいいのです(次のエントリに書きました)。
式コメント中で使う関数を新しく定義するのは十分検討したほうがいいですが(実はF#やC#のコードで実現したほうがわかりやすいのでは?とか)、SQL中のバインドする場所の近くに記述できるので飛躍的に可読性が上がることがあります。そんなときに使ってもらえればと思います。