Haskell Advent Calendar 2012の6日目のエントリです。
今回は複雑な SQL Queryを型安全に書くためのライブラリ HaskellDB を紹介します。
Queryの記述例
さっそくですが簡単な Query を HaskellDB で書いてみます。
簡単のために疑似コードにしてありますが、以下ような型のフィールド名を持つテーブルを2つ考えてみます。
Password.hs
-- password -- テーブル名 -- userName :: String -- uid :: Int -- gid :: Int
Group.hs
-- group -- テーブル名 -- groupName :: String -- gid :: Int
まずは細かいことを気にせずに Query の記述の気分を体験してみましょう。
必要な定義が揃っていれば、以下の queryRoot のように Query を書くことができます。
sample.hs
import Password (Password, password) import Database.HaskellDB (Query, Select, Rel, table, restrict, (!), (.==.), constant) queryRoot :: Query (Rel Password) queryRoot = do pwd <- table password restrict $ pwd ! Password.uid .==. constant 0 return pwd -- 展開結果のSQL -- SELECT user_name, -- uid, -- gid -- FROM SCHEMA0.password as T1 -- WHERE uid = 0
table で定義済のテーブル password からテーブルで表現される関係の Query を作り、restrict で条件を記述しています。
Query 型は Show の instance にもなっていて、結果としてどのような SQL に展開されるかを文字列で取り出すこともできます。
queryRoot の SQL への展開結果は以上のコメント部分に書いてみました。
つぎに結合した式も書いてみましょう(queryJoin0)。
sample1.hs
import Password (password) import Group (group) import Database.HaskellDB (Query, Select, table, restrict, project, (!), (.==.), (#), (<<), constant) queryJoin0 = do pwd <- table password grp <- table group restrict $ pwd ! Password.gid .==. grp ! Group.gid project (Password.uid << pwd ! Password.uid # Password.userName << pwd ! Password.userName # Group.groupName << grp ! Group.groupName ) -- 展開結果のSQL -- SELECT uid1 as uid, -- user_name1 as user_name, -- group_name2 as group_name -- FROM (SELECT group_name as group_name2, -- gid as gid2 -- FROM SCHEMA0.group as T1) as T1, -- (SELECT user_name as user_name1, -- uid as uid1, -- gid as gid1 -- FROM SCHEMA0.password as T1) as T2 -- WHERE gid1 = gid2
Query を do の中に並べることで関係の結合の Query を表現することができます。
ここではテーブル password と group で表現される関係の直積の Query を記述し、restrict で gid が一致するものだけに制限しています。
do の最後の式の project は、結合した結果から取り出すフィールドを選択しています。
以上のように、型検査された代数的な結合式の定義から、SQLの結合式を生成することができました。
Query を定義することで結合式を部品化することになるので、別の結合式の一部として再利用することができます。
以下はあまりおもしろくない例ですが、queryJoin0 を再利用してより大きな結合式を記述しています。
sample1a.hs
queryJoin1 = do j0 <- queryJoin0 pwd <- table password restrict $ j0 ! Password.uid .<. pwd ! Password.uid project (nameA << j0 ! Password.name # nameB << pwd ! Password.name ) -- 展開結果のSQL -- SELECT name3 as name_a, -- name4 as name_b -- FROM (SELECT name as name4, -- uid as uid4 -- FROM PUBLIC.password as T1) as T1, -- (SELECT uid1 as uid3, -- name1 as name3, -- name2 as name3 -- FROM (SELECT name as name2, -- gid as gid2 -- FROM PUBLIC.group as T1) as T1, -- (SELECT name as name1, -- uid as uid1, -- gid as gid1 -- FROM PUBLIC.password as T1) as T2 -- WHERE gid1 = gid2) as T2
文字列で直接 SQL を書くときとは違って、細かい文法の間違いや暗黙の型変換が起こることはありません。Haskell で型を検査しながら安全に複雑な Query を書くことができます。
また、そのように定義した Query を複数箇所で再利用してより大きなQueryを記述できるのも、直接 SQL を書く場合に比べた利点です。
HaskellDBを利用するときに必要な定義
HaskellDB はすばらしいライブラリですが、利用するための定義の記述が面倒です。
HaskellDB でテーブルに対する Query を記述するには、対象のテーブルを定義しておく必要があります。
また、Query 結果である関係にフィールドの値による制限を付加するにはフィールドを表現する式と型を定義しておく必要があります。
テーブルは以下のように定義します。
テーブルの定義(以下の例ではgroup)は table で Query を表現するのに必要となります。
Group.hs
-- 関係の型の定義 type Group = RecCons GroupName (Expr String) (RecCons Gid (Expr Int) RecNil) -- テーブルの定義 group :: Table Group group = baseTable "SCHEMA0.group" ((hdbMakeEntry GroupName) # (hdbMakeEntry Gid))
フィールドを表現する式と型は以下のように書きます。
フィールドの定義(以下の例ではgroupName)は、関係から特定のフィールドを指定したり、Query を実行した結果であるレコードから値を取り出すために使います。フィールドの定義の型を FieldTag のインスタンンスにすることで SQL におけるフィールド名を指定しています。
Group.hs
data GroupName = GroupName instance FieldTag GroupName where fieldName _ = "group_name" groupName :: Attr GroupName String groupName = mkAttr GroupName
このような定義がフィールドごと必要となり大変です。
Template Haskellによる定義
フィールドの名前と型のリストから、
テーブルやフィールドを表現する式を Template Haskell で生成するとスマートです。
以下の例では mkRelationType ではフィールドの名前と型のリストからテーブルの型と値を定義する Haskell の式を組みたてています。
defineFieldExpr でフィールドを表現する式の定義を組みたて、defineFieldType でフィールドを表現する式の型の定義とその FieldTag のインスタンスを定義する式を組みたてています。
TH.hs
mkRelationType :: [(Name, TH.TypeQ)] -> TH.TypeQ mkRelationType = foldr (\(n,e) exp' -> [t|RecCons $(conT n) (Expr $e) $exp'|]) [t|RecNil|] defineFieldExpr attrName ypeName typeQ = do -- フィールドの表現の型シグネチャ fieldS <- sigD attrName [t|Attr $(conT typeName) $typeQ|] -- フィールドの表現 fieldF <- valD (varP attrName) (normalB [|mkAttr $(conE typeName)|]) [] return [fieldS, fieldF] defineFieldType typeName colName = do fieldD <- dataD (cxt []) typeName [] [normalC typeName []] [] fieldI <- [d| instance FieldTag $(conT typeName) where fieldName _ = $(litE (stringL colName)) |] return $ fieldD : fieldI
Template Haskell は便利なのですが、GHC のバージョンアップによって互換性が壊れやすいと言われています。
次の2つに注意して書くと良いと私は考えています。
ライブラリにしてみた
HaskellDB まわりのよく使いそうな定義を Template Haskell を使って生成するライブラリを作ってみました。