Help us understand the problem. What is going on with this article?

Haskell から MySQL を使う

More than 3 years have passed since last update.

ひょんなことからHaskellで簡単な掲示板を作る機会があったので
その時に覚えたmysql-simpleの使い方を書き留めておこうと思います。

環境構築

テスト用のディレクトリとcabalファイルを作ってmysql-simpleを入れます

$ mkdir mysql-test && cd mysql-test
$ cabal sandbox init

mysql-test.cabal を作ります。これはライブラリの依存関係を記述するファイルです。

$ vim mysql-test.cabal
name: mysql-test
version: 0.0.0.1
build-type: Simple
cabal-version: >=1.20

executable app
  main-is: app.hs
  build-depends: base, mysql-simple
  ghc-options: -Wall
  default-language: Haskell2010

依存ライブラリをインストールしましょう。

$ cabal install --only-dependencies

実際にアプリケーションを書いていきます

$ vim app.hs
{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo
  [Only i] <- query_ conn "select 2 + 2"
  print (i :: Int)

実行します。このときMySQLサーバーが立ち上がってることを確認して下さい。

$ cabal run app
4

4 が表示されれば成功です!

テスト用のDBを作っておきましょう。

$ mysql -uroot
mysql> CREATE DATABASE mysql_test;
mysql> USE mysql_test;
mysql> CREATE TABLE user (id int unsigned, name varchar(255), PRIMARY KEY (id));

CRUD

CREATE

{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }
  execute conn "INSERT INTO user VALUES (?, ?)" (1 :: Int, "a" :: String)
  return ()

実行すると確かにデータが挿入されているはずです

mysql> SELECT * FROM user;
+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+

main関数を一行ずつ見て行きましょう
最初の行はMySQLとの接続をしています

conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }

connectdefaultConnectInfoの情報を調べてみましょう。

$ ghci
Prelude> :m Database.MySQL.Simple

Prelude> :t connect
connect :: ConnectInfo -> IO Connection

Prelude> :t defaultConnectInfo
defaultConnectInfo :: ConnectInfo

Prelude> :i ConnectInfo
data ConnectInfo
  = ConnectInfo {connectHost :: String,
                 connectPort :: GHC.Word.Word16,
                 connectUser :: String,
                 connectPassword :: String,
                 connectDatabase :: String,
                 connectOptions :: [Database.MySQL.Base.Types.Option],
                 connectPath :: FilePath,
                 connectSSL :: Maybe Database.MySQL.Base.SSLInfo}

Prelude> defaultConnectInfo
ConnectInfo {connectHost = "localhost", connectPort = 3306, connectUser = "root", connectPassword = "", connectDatabase = "test", connectOptions = [CharsetName "utf8"], connectPath = "", connectSSL = Nothing}

ここまで見れば明らかですがdefaultConnectInfoConnectInfo型でこれはMySQLに接続するために必要な情報を格納するレコードです。connectはこれを引数にとってMySQLと接続するための関数です。defaultConnectInfoのままではtestのDBを見に行ってしまうのでレコード構文を使って先ほど作ったDBを見に行くように変えています。

defaultConnectInfo { connectDatabase = "mysql_test" }

次の行は実際にデータを挿入しているところです

execute conn "INSERT INTO user VALUES (?, ?)" (1 :: Int, "a" :: String)

mysql-simpleにはqueryexecuteの2つのクエリを実行する関数があります。queryはSELECT文のように何か結果を返すようなクエリの実行に、executeはINSERTやUPDATEのような何も結果を返さないようなクエリの実行に使います。今回はINSERTなのでexecuteを使っています。
クエリの中の"?"は後の引数のタプルの要素がサニタイジングされて代入されます。クエリ中の"?"の個数とタプルの要素数は一致していないといけません。
(1 :: Int, "a" :: String)の部分は少し見にくいですが本質的には(1,"a")のタプルで、リテラルだと型がわからないので型注釈が入った形になっています。

最後の

return ()

の行はmain関数の型を合わせるために入っています。

一気に十人ぐらいのデータを入れてみましょう。

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad
import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }
  forM (zip [2..10] ['b'..]) $ \(n, c) -> do
    execute conn "INSERT INTO user VALUES (?, ?)" (n :: Int, [c])
  return ()

READ

先ほど入れたデータを今度は読んでみましょう

{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }
  rs <- query_ conn "SELECT * FROM user"
  mapM_ putStrLn $ map show (rs :: [(Int, String)])

実行すると

$ cabal run app
(1,"a")
(2,"b")
(3,"c")
(4,"d")
(5,"e")
(6,"f")
(7,"g")
(8,"h")
(9,"i")
(10,"j")

ちゃんと表示されると思います。

query_, execute_はquery, executeとほとんど同じですが、クエリ中に"?"が無くタプルを引数としてとらない場合に使います。
実行結果は(rs :: [(Int, String)])のように正しく型付けしてやる必要があります。

type User = (Int, String)

のようにしておけば(rs :: [User])のように書くことも出来ます。

UPDATE, DELETE

ここまでくればUPDATEとDELETEは自然に書けると思います。

UPDATE

{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }
  execute_ conn "UPDATE user SET name = 'aa' WHERE id = 1"
  return ()

DELETE

{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple

main :: IO ()
main = do
  conn <- connect defaultConnectInfo { connectDatabase = "mysql_test" }
  execute_ conn "DELETE FROM user WHERE id = 10"
  return ()

簡単なアプリを作ってみる

最後にさっき作ったuserテーブルを読み書きするだけの簡単なアプリを作ってみます

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad
import Database.MySQL.Simple

type User = (Int, String)

getConnection:: IO Connection
getConnection = connect defaultConnectInfo { connectDatabase = "mysql_test" }

mySelect :: IO ()
mySelect = do
  conn <- getConnection
  rs   <- query_ conn "SELECT * FROM user"
  mapM_ putStrLn $ map show (rs :: [User])

myInsert :: IO ()
myInsert = do
  putStrLn "Data: "
  input <- getLine
  let user = (read $ concat ["(", input, ")"]) :: User
  conn <- getConnection
  execute conn "INSERT INTO user VALUES (?, ?)" user
  return ()

myUpdate :: IO ()
myUpdate = do
  putStrLn "Data: "
  input <- getLine
  let (n, name) = (read $ concat ["(", input, ")"]) :: User
  conn <- getConnection
  execute conn "UPDATE user SET name = ? WHERE id = ?" (name, n)
  return ()

myDelete :: IO ()
myDelete = do
  putStrLn "ID: "
  input <- getLine
  let n = read input :: Int
  conn <- getConnection
  execute conn "DELETE FROM user WHERE id = ?" (Only n)
  return ()

main :: IO ()
main = do
  putStrLn "Command: "
  command <- getLine
  case command of
    "show"   -> mySelect >> main
    "insert" -> myInsert >> main
    "update" -> myUpdate >> main
    "delete" -> myDelete >> main
    "exit"   -> putStrLn "Bye." >> return ()
    _        -> putStrLn "Command: show, insert, update, deletem or exit" >> main

実際にコピペして実行してみてください。
以下のような感じで遊ぶことが出来ます

$ cabal run app
Command:
show
(1,"aa")
(2,"b")
(3,"c")
(4,"d")
(5,"e")
(6,"f")
(7,"g")
(8,"h")
(9,"i")
Command:
insert
Data:
10, "haskell"
Command:
delete
ID:
8
Command:
show
(1,"aa")
(2,"b")
(3,"c")
(4,"d")
(5,"e")
(6,"f")
(7,"g")
(9,"i")
(10,"haskell")
Command:
exit
Bye.
lotz
実用関数型プログラミング言語 Haskell の情報を発信しています
http://lotz84.github.io/
folio-sec
誰もがかんたんに資産運用することができるサービス「フォリオ」を作っているFinTech系スタートアップ
https://corp.folio-sec.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away