HaskellDB と Template Haskell

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つに注意して書くと良いと私は考えています。

  • Haskellの文法が変わることは少ないはずなので、できるだけ Quote ([| |], [t| |], [d| |], [p| |]) を使って書く
  • データコンストラクタ (AppE等) を使って書くと Template Haskell のデータ定義に強く依存してしまうので、Q Monad を生成する関数 (appE等) を使って書く

ライブラリにしてみた

HaskellDB まわりのよく使いそうな定義を Template Haskell を使って生成するライブラリを作ってみました。

https://github.com/khibino/haskelldb-genschema

まとめ

  • HaskellDB は複雑な SQL Query を安全に書くのに便利
  • しかし Queryを書くのに必要な定義が多くて準備が面倒
  • 定義を Template Haskell で生成するライブラリを書いてみました