Database.Esqueleto入門

みんなこんにちは！liquid_amberです。業務ではC++開発をしています。ただC++こわいのと、gcc 4.4のC++0xの実装が古すぎて辛い。あとRustとPureScriptは気になるけどほとんど触ってない。なので、今日はHaskellのSQLを楽に書ける言語内DSLであるEsqueletoの話をします。

## TL;DR

* HaskellでDBアクセスにはpersistentつかえ
  * って言うけどRDBMS特化してないじゃん, SQL書けるの？
* RDBMSアクセスにはpersistent+esqueleto使え
  * SQL直書き気分で文字列とか悩まされずに書ける
* サンプルソースコードは[Gistに上げた](https://gist.github.com/liquidamber/d38765290726e175f0e6)ので見てくれ

## 経緯

今[こういうもの][nayoro]を片手間に作っているんですが、Haskellで書いています。何かって言うとRDBMS使う小規模なウェブアプリ、すごい普通のウェブアプリなんですが、[HaskellでWebAppの開発に必要なN個のこと - maoeのブログ](http://maoe.hatenadiary.jp/entry/2013/09/10/182914)とか見ても分かる通り最近のHaskell web framework三国時代はSnap, Happstack, Yesodになってる。で、Yesod陣営を使おう、Web frontendの部分に関してはYesodは重量級なんで[scotty][scotty]使おうかなーって思ってたんですが、ググってみると[apiary][apiary]ってのが良さげだったのでこっちを使うことに（[apiary.io][apiary.io]ってサイトとは全く関係ない）。そうなるとフロントは決まったなってことでバックエンドでDB触る話が今日のお題です。

そもそもWeb frameworkとDB触る層関係ないだろッて思われるかもしれませんが、Haskellではproducer/consumer型のI/Oライブラリが乱立していて、それぞれを率いるのがWeb frameworkの開発者という状態。それで、[Yesod][yesod]の開発者はconduitというI/Oライブラリ上に

* HTTPサーバ warp
* webアプリのI/F wai (WSGI的なもの)
* DBライブラリ[persistent][persistent]

などなどいろいろ作っていて、今日紹介するのはその最後の部分、僕の使うapiaryはwaiに準拠したフレームワークなのでYesod陣営ということになってこのスタックに載ってくる。

## esqueleto入門
### テーブルを定義しよう

persistentというのはDBアクセスを型安全にしてくれるライブラリです、だいたいORMみたいなもんです。バックエンドとして色々選べます(sqlite, MongoDB, MySQL, PostgreSQL, …)。で、TemplateHaskellを使ってメタメタしくしてくれるのがポイント。次みたいなコードを書くとデータ型定義とかテーブル定義とか色々してくれるらしい。

```haskell
{- たくさんの言語拡張, 詳しくはgistみて欲しい -}
import Data.Persistent.TH

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
  mail Text -- フィールド定義
  UniqueMail mail
  deriving Show

Handle
  personId PersonId
  service Text
  name Text
  created UTCTime
  UniqueHandle personId service name created
  deriving Show

Uri
  handleId HandleId
  uri Text
  label Text
  UniqueUri handleId uri label
  deriving Show
|]
```

これを書くとTemplateHaskellの御加護で勝手に`Person`, `Handle`, `Uri`というデータ型が宣言されて便利。更に、例えば`Person`に対応する主キーの型が`PersonId`という型で定義されていたり、`UniqueXXX`って書いておけば`unique`キー制約をつけられたりします。因みにDBからPersonを取得すると`Entity Person`っていう型の`Entity{entityVal = Person(..), entityKey = PersonId(..)}`みたいなものになって出てくるのは要注意です。

### 普通のCRUD
これでモデルの定義ができたんで、ひと通りデータのCRUDができる。

```haskell
import qualified Data.Text as T

main = do
  runSqlite ":memory:" $ do -- 以下をsqliteで実行
    time <- liftIO getCurrentTime -- 時刻取得
    runMigration migrateAll -- テーブルのmigration

    -- データの追加, insertするとPersonId型の主キーが取れる
    johnId <- insert $ Person "john@example.com"
    janeId <- insert $ Person "jane@example.com"
    johnWId <- insert $ Person "john_watson@example.com"

    -- 以下relationを作るために上で得たIDを入れている
    johnExampleId <- insert $ Handle johnId "example" "John Doe" time
    janeExampleId <- insert $ Handle janeId "example" "Jane Doe" time
    insert $ Handle johnId "irc" "john_doe" time
    johnAnotherId <- insert $ Handle johnWId "another" "John Watson" time

    insert $ Uri johnExampleId "calendar" "http://example.com/calendar/john"
    insert $ Uri johnExampleId "address book" "http://example.com/address/john"
    insert $ Uri johnAnotherId "irc" "http://example.com/calendar/john_watson"

    liftIO $ putStrLn "use normal ==. operator"
    johnHandles <- selectList [HandleName ==. "John Doe"] [LimitTo 1]
    forM_ johnHandles $ \johnHandle -> do
      johnURIs <- selectList [UriHandleId ==. entityKey johnHandle] []
      liftIO $ print (johnURIs :: [Entity Uri])
```

`insert`で新規作成、`selectList`で取得できるから（後削除とか更新とかもある）大体全部できるっぽい。やった！と思ったけどここで疑問に思うことでしょう。何が疑問かというと、persistentって上で言ったとおりバックエンドが幾多もあってMongoDBとかも選べるものです。ということは、DBへの仮定は結構弱くて、検索条件ほとんど選べないんじゃないかって言う気がします。上で見たのは「`Handle`のentityのうち`name` fieldが`"John Doe"`と一致するもの」というのを検索してたわけだけど、実際等号不等号、大小比較、属するしない程度しか条件がない。

### PersistentでSQL書きたい

不幸ながら、私が今回検索したいのは「`"john"`を部分文字列に含む〜」要はSQLで言う`LIKE`がしたいので条件がしょぼいとそういうことはできない。これに対してかなりprimitiveな解法として存在するのが[`rawSql`][persistent_rawsql]というinterface。例えばこれでLIKEを使って"john"を含む`Handle`のエンティティを検索してみよう。あと、ついでに含まれる`Handle`毎に`Uri`を都度検索するのだるいし遅そうだからOUTER JOINしよう、なんせSQLなんだから。

```haskell
    let s = "SELECT ??, ?? FROM handle LEFT OUTER JOIN uri ON handle.id = uri.handle_id WHERE handle.name LIKE '%' || ? || '%'"
    johnH2 <- rawSql s [toPersistValue ("john" `asTypeOf` T.empty )]
    liftIO $ forM_ (johnH2 :: [(Entity Handle, Maybe (Entity Uri))]) print
```

なんと、びっくりするくらい抽象度が下がりました。

* `rawSql :: Text -> [PersistValue] -> なんかMonad`という型
    * 当たり前ですがSQL間違ってたら実行時エラーになる
    * 第2引数はプレースホルダに与える値なんですが
        * 個数間違っても実行時エラー
        * そもそも文字列を期待している箇所に数値渡しても実行時エラー
* テーブルの名前とかも唐突に出てくる
    * persistentが`Person`のテーブル名は`person`だとか知ってるんだから勝手にしておいて欲しい
* フィールド名も唐突に出てくる

唯一取り出すフィールド名は列挙せずに`??`で済んでいることが救いです。とにかくこんな生SQL地獄は抜けて[esqueleto][esqueleto]使おう。このEsqueletoっていうのはSQLの言語内DSLでこんなかんじで書ける。

```haskell
select
  $ from $ \(entity1 `InnerJoin` entity2 `LeftOuterJoin` …) -> do
  on $ 一番最後のjoinのon
  on $ …
  on $ 最初のjoinのon
  where_ $ where_節
  orderBy $ [asc (条件), dsc(条件), …]
  limit N -- してみたり
  groupBy -- してみたりできる
  return (entity1, entity2 ^. Entity2SomeField)
```

具体例を見てみよう。

```haskell
import qualified Database.Esqueleto as E
import Database.Esqueleto ((^.), (?.))
-- 続き
    johnEs <-
      E.select $ E.from $ \(handle `E.LeftOuterJoin` muri) -> do
        E.on $ E.just (handle ^. HandleId) E.==. muri ?. UriHandleId
        E.where_ $ handle ^. HandleName `E.like` (E.%) E.++. E.val "john" E.++. (E.%)
        return (handle, muri)
    liftIO $ forM_ johnEs print
```

ほらやっぱりSQLだ！

* `where`は完全に予約語なので`where_`になっている
* `on`がjoinと順番逆な点に注意。
* `^.`でエンティティのフィールドを参照できる
  * `?.`はちょうどCoffeeScriptの`?.`みたいな感じ、Haskellっぽく言うとMaybeに持ち上げてフィールドを参照できる。
* `E.on`の中で`E.just`とかやってMaybeっぽくしてるのは外部結合だから
  * `muri`は`NULL`かも知れないので型としては`Maybe (Entity Uri)`
  * 実は気にせず`E.on handle ^. HandleId E.==. muri ^. UriHandleId`って書いてもコンパイルは通っちゃう
  * そんでもって`muri`に`NULL`が来ると例外で落ちるからちょっとつらい
* `E.where_`で文字列結合しててSQLインジェクション大丈夫なのか！？
  * とお思いの方に朗報ですがもちろん`E.val`(Haskellの値を埋め込む関数)自体がプレースホルダで実装されてるので気にしないでいいです

っていう感じで素晴らしいのでぜひ使ってみてください。

## まだわかってないこと

* シャードとかマスタースレーブ構成とかどうすんだろ
* トランザクションってどうやって扱うんだろう？

## 蛇足

```console
$ # gistに上げたコードを実行する方法
$ cabal sandbox init
$ cabal install --only-dependencies --enable-tests
$ cabal configure
$ cabal build
$ ./dist/build/adv-cal2014/adv-cal2014
```

[nayoro]: https://github.com/liquidamber/nayoro
[HaskellPlatform]: https://www.haskell.org/platform/
[ghc]: https://www.haskell.org/ghc/
[scotty]: https://github.com/scotty-web/
[apiary]: https://github.com/philopon/apiary/
[apiary.io]: http://apiary.io/
[yesod]: http://www.yesodweb.com/
[persistent]: https://hackage.haskell.org/package/persistent
[yesodbook_persistent]: http://www.yesodweb.com/book/persistent
[yesodbook_sqljoin]: http://www.yesodweb.com/book/sql-joins
[persistent_rawsql]: https://github.com/yesodweb/yesod/wiki/RawSQL
[esqueleto]: https://hackage.haskell.org/package/esqueleto