HaskellでMySQLを使う時、僕はよくmysql-simpleを使っています。このライブラリはすごく簡単に使えるので重宝しているのですが更新が止まってたりいろいろ不満も多いです。先日Redditを眺めていたらCのライブラリのFFIではなく1からHaskellで実装したMySQLドライバが話題になっていたのでさっそく試してみました。
Pure haskell mysql driver
winterland1989/mysql-haskell
READMEにはmysql-simple
に不満なところが3つ挙げられています。
- プリペアドステートメントとバイナリプロトコルがサポートされていない
- FFIのせいで並列処理に限界がある
- レプリケーションプロトコルのサポートがない
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
をこんな感じにして
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
をこんな感じにして
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
をこんな感じにして
{-# 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-streamsのInputStreamになっているのでストリーム処理も楽々です(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
に戻りましょう。手始めに今挿入したデータを取得してみます。
{-# 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}]
このようなカラムに関する情報がつめ込まれています。
さて今度はデータを挿入してみましょう。
{-# 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
を使って実行するようです。
さっそく実行してみましょう
$ 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
の動的プレースホルダーを試してみましょう
{-# 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_
の _
の意味はパラメータを取らないぞ!ということだったんですね。
次に静的プレースホルダーを試してみましょう
{-# 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用のコメントもあるのでとても読みやすいですよー