Haskellで実装された MySQL ドライバ mysql-haskell を試してみた

HaskellでMySQLを使う時、僕はよく[mysql-simple](https://hackage.haskell.org/package/mysql-simple)を使っています。このライブラリはすごく簡単に使えるので重宝しているのですが更新が止まってたりいろいろ不満も多いです。先日Redditを眺めていたらCのライブラリのFFIではなく1からHaskellで実装したMySQLドライバが話題になっていたのでさっそく試してみました。

> Pure haskell mysql driver
[winterland1989/mysql-haskell](https://github.com/winterland1989/mysql-haskell)

[README](https://github.com/winterland1989/mysql-haskell#motivation)には`mysql-simple`に不満なところが3つ挙げられています。

1. プリペアドステートメントとバイナリプロトコルがサポートされていない
2. FFIのせいで並列処理に限界がある
3. レプリケーションプロトコルのサポートがない

`mysql-haskell`はこれらの問題を解決することをモチベーションにしています。
パフォーマンスに関してもREADMEに[丁寧なベンチマーク](https://github.com/winterland1989/mysql-haskell#is-it-fast)が載せられていてC/C++よりは2倍遅くなるが`mysql-simple`より5倍速くなるぐらいだそうです。

使ってみる
---------
`mysql-haskell`はHackageにもStackageにも登録されていませんが[stack](https://docs.haskellstack.org/en/stable/README/)のおかげで簡単に使ってみる事ができます。

```bash
$ stack new mysql-haskell-test
$ cd mysql-haskell-test
```

まずは空のプロジェクトを作成しましょう。
`stack.yaml`をこんな感じにして


```yaml:stack.yaml
resolver: lts-6.11
packages:
- '.'
- location:
    git: git@github.com:winterland1989/mysql-haskell.git
    commit: 5f880b6ae1f3636098e6ff5e3a545b161ef32195
extra-deps:
- binary-0.8.4.1
- HsOpenSSL-x509-system-0.1.0.2
- tcp-streams-0.3.0.0
- wire-streams-0.0.2.0
flags: {}
extra-package-dbs: []
```

`mysql-haskell-test.cabal`をこんな感じにして

```yaml:mysql-haskell-test.cabal
name:                mysql-haskell-test
version:             0.1.0.0
build-type:          Simple
cabal-version:       >=1.10

executable app
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , io-streams
                     , mysql-haskell
  default-language:    Haskell2010
```

`app/Main.hs` をこんな感じにして

```hs:app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Database.MySQL.Base
import qualified System.IO.Streams as Streams

main :: IO ()
main = do
  conn <- connect defaultConnectInfo
  (defs, is) <- query_ conn "SELECT 2 + 2"
  print =<< Streams.toList is
```

実行してみましょう。

```bash
$ stack build
$ stack exec app
[[MySQLInt64 4]]
```

見ればわかると思いますがクエリの結果は[io-streams](http://hackage.haskell.org/package/io-streams)の[InputStream](http://hackage.haskell.org/package/io-streams/docs/System-IO-Streams-Internal.html#t:InputStream)になっているのでストリーム処理も楽々です（io-streamsなんですねー）

遊んでみる
-----------------

Hello World 的なのは終わったのでちゃんとDBにテーブルを用意して遊んでみましょう。

```bash
$ mysql -uroot -p
mysql> CREATE DATABASE testMySQLHaskell;
mysql> USE testMySQLHaskell
mysql> CREATE TABLE person (
    ->   id int unsigned NOT NULL AUTO_INCREMENT,
    ->   name varchar(255) NOT NULL,
    ->   age int unsigned NOT NULL,
    ->   PRIMARY KEY (id)
    -> );
Query OK, 0 rows affected (0.03 sec)

mysql> INSERT INTO person VALUES (NULL, 'lotz', 25);
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO person VALUES (NULL, 'alice', 24);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM person;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | lotz  |  25 |
|  2 | alice |  24 |
+----+-------+-----+
2 rows in set (0.00 sec)
```

こんな感じのテーブルとデータを用意します。
（以下コピペ用）

```sql
CREATE TABLE person (
  id int unsigned NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  age int unsigned NOT NULL,
  PRIMARY KEY (id)
);
INSERT INTO person VALUES (NULL, 'lotz', 25);
INSERT INTO person VALUES (NULL, 'alice', 24);
```

`mysql-haskell`に戻りましょう。手始めに今挿入したデータを取得してみます。

```hs:app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Database.MySQL.Base
import qualified System.IO.Streams as Streams

main :: IO ()
main = do
  conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
  (defs, is) <- query_ conn "SELECT * FROM person"
  print =<< Streams.toList is
```

変更したのは

* `ciDatabase` でデータベースを選択
* クエリ `"SELECT * FROM person"`

の2箇所だけです。実行してみましょう

```bash
$ stack build
$ stack exec app
[[MySQLInt32U 1,MySQLText "lotz",MySQLInt32U 25],[MySQLInt32U 2,MySQLText "alice",MySQLInt32U 24]]
```

`mysql-haskell`は非常にプリミティブなライブラリなので独自のレコードへマッピングするような機能はありません（少なくとも今は）。ちなみに `defs` には

```hs
[ColumnDef {columnDB = "testmysqlhaskell", columnTable = "person", columnOrigTable = "person", columnName = "id", columnOrigName = "id", columnCharSet = 63, columnLength = 10, columnType = FieldType 3, columnFlags = 16931, columnDecimals = 0},ColumnDef {columnDB = "testmysqlhaskell", columnTable = "person", columnOrigTable = "person", columnName = "name", columnOrigName = "name", columnCharSet = 33, columnLength = 765, columnType = FieldType 253, columnFlags = 4097, columnDecimals = 0},ColumnDef {columnDB = "testmysqlhaskell", columnTable = "person", columnOrigTable = "person", columnName = "age", columnOrigName = "age", columnCharSet = 63, columnLength = 10, columnType = FieldType 3, columnFlags = 4129, columnDecimals = 0}]
```

このようなカラムに関する情報がつめ込まれています。

さて今度はデータを挿入してみましょう。

```hs:app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Exception
import Database.MySQL.Base
import qualified System.IO.Streams as Streams

transactional :: MySQLConn -> IO a -> IO a
transactional conn procedure = mask $ \restore -> do
  execute_ conn "BEGIN"
  a <- restore procedure `onException` (execute_ conn "ROLLBACK")
  execute_ conn "COMMIT"
  pure a

main :: IO ()
main = do
  conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
  transactional conn $ do
    execute_ conn "INSERT INTO person VALUES (NULL, 'bob', 26)"
    (defs, is) <- query_ conn "SELECT * FROM person"
    mapM_ print =<< Streams.toList is
```

`BEGIN` や `INSERT` は `query` ではなく `execute` を使って実行するようです。
さっそく実行してみましょう

```bash
$ stack build
$ stack exec app
[MySQLInt32U 1,MySQLText "lotz",MySQLInt32U 25]
[MySQLInt32U 2,MySQLText "alice",MySQLInt32U 24]
[MySQLInt32U 3,MySQLText "bob",MySQLInt32U 26]
```

`SELECT` の結果が見やすいように `mapM_` を使ってみました。

プレースホルダー
--------------

実際のアプリケーションで安全にSQL文を組み立てるためにはプレースホルダーが便利です（というか必須？）。プレースホルダーというのはクエリを `INSERT INTO person VALUES (null, ?, ?)` のように `?` を使った形で書いてパラメータを後で渡せるようになってるものです。このパラメータをDBに行ってからで渡すのとライブラリで先に渡してしまうやり方にそれぞれ **静的プレースホルダー**, **動的プレースホルダー** という名前がついています。`mysql-simple`もプレースホルダーを使うことが出来るのですがサポートしてるのは動的プレースホルダーだけです。`mysql-haskell`ではプリペアドステートメントを扱う関数が用意されており静的プレースホルダーを使うこともできます。

まずは`mysql-haskell`の動的プレースホルダーを試してみましょう

```hs:app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Exception
import Database.MySQL.Base
import qualified System.IO.Streams as Streams

transactional :: MySQLConn -> IO a -> IO a
transactional conn procedure = mask $ \restore -> do
  execute_ conn "BEGIN"
  a <- restore procedure `onException` (execute_ conn "ROLLBACK")
  execute_ conn "COMMIT"
  pure a

main :: IO ()
main = do
  conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
  transactional conn $ do
    execute conn "INSERT INTO person VALUES (NULL, ?, ?)" [MySQLText "chris", MySQLInt32 18]
    (defs, is) <- query_ conn "SELECT * FROM person"
    mapM_ print =<< Streams.toList is
```

変わったのはここだけです

```diff
 main = do
   conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
   transactional conn $ do
-    execute_ conn "INSERT INTO person VALUES (NULL, 'bob', 26)"
+    execute conn "INSERT INTO person VALUES (NULL, ?, ?)" [MySQLText "chris", MySQLInt32 18]
     (defs, is) <- query_ conn "SELECT * FROM person"
     mapM_ print =<< Streams.toList is
```

`execute_` の `_` の意味はパラメータを取らないぞ！ということだったんですね。

次に静的プレースホルダーを試してみましょう

```hs:app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Exception
import Database.MySQL.Base
import qualified System.IO.Streams as Streams

transactional :: MySQLConn -> IO a -> IO a
transactional conn procedure = mask $ \restore -> do
  execute_ conn "BEGIN"
  a <- restore procedure `onException` (execute_ conn "ROLLBACK")
  execute_ conn "COMMIT"
  pure a

main :: IO ()
main = do
  conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
  stmt <- prepareStmt conn "INSERT INTO person VALUES (NULL, ?, ?)"
  transactional conn $ do
    executeStmt conn stmt [MySQLText "chris", MySQLInt32 18]
    (defs, is) <- query_ conn "SELECT * FROM person"
    mapM_ print =<< Streams.toList is
```

動的プレースホルダーの例とのdiffはこんな感じ

```diff
 main :: IO ()
 main = do
   conn <- connect defaultConnectInfo {ciDatabase = "testMySQLHaskell"}
+  stmt <- prepareStmt conn "INSERT INTO person VALUES (NULL, ?, ?)"
   transactional conn $ do
-    execute conn "INSERT INTO person VALUES (NULL, ?, ?)" [MySQLText "chris", MySQLInt32 18]
+    executeStmt conn stmt [MySQLText "chris", MySQLInt32 18]
     (defs, is) <- query_ conn "SELECT * FROM person"
     mapM_ print =<< Streams.toList is
```

MySQLのプリペアドステートメントそのままで `1. あらかじめクエリを登録して` `2. 取得したステートメントにパラメータを渡して実行する` という手順になっています。

おわりに
-------

`mysql-haskell`は出たばっかりのライブラリなのでここで紹介したインターフェースもどんどん変更されていく可能性があります。それでも`mysql-simple`にとって代わろうとする新しいライブラリということで紹介せずにはいられませんでした。更にいろいろ試してみたいという人にはドキュメントを読んで欲しいところですが僕が弱いせいかHaddockをビルドできなかったので、とりあえず直に[実装を読む](https://github.com/winterland1989/mysql-haskell/tree/master/Database/MySQL)ことをオススメします。 幸いまだ分量も少ないしHaddock用のコメントもあるのでとても読みやすいですよー