Haskell
MySQL

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

More than 1 year has passed since last update.

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

Pure haskell mysql driver
winterland1989/mysql-haskell

READMEにはmysql-simpleに不満なところが3つ挙げられています。

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

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

使ってみる

mysql-haskellはHackageにもStackageにも登録されていませんがstackのおかげで簡単に使ってみる事ができます。

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

まずは空のプロジェクトを作成しましょう。
stack.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をこんな感じにして

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 をこんな感じにして

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

実行してみましょう。

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

見ればわかると思いますがクエリの結果はio-streamsInputStreamになっているのでストリーム処理も楽々です(io-streamsなんですねー)

遊んでみる

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

$ 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)

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

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に戻りましょう。手始めに今挿入したデータを取得してみます。

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箇所だけです。実行してみましょう

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

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

[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}]

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

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

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

BEGININSERTquery ではなく execute を使って実行するようです。
さっそく実行してみましょう

$ 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の動的プレースホルダーを試してみましょう

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

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

 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__ の意味はパラメータを取らないぞ!ということだったんですね。

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

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はこんな感じ

 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をビルドできなかったので、とりあえず直に実装を読むことをオススメします。 幸いまだ分量も少ないしHaddock用のコメントもあるのでとても読みやすいですよー