Database in Haskell with Servant
peristent を使って Servant server を実装する方法を紹介します。
本記事のソースコードは以下のリポジトリにおいてあります。
http://github.com/algas/haskell-servant-cookbook
Model
Template Haskell を使って User 型を定義します。
要素はこれまでのサンプルで使っていたものと同じです。
Persistent のモデルであると同時に JSON instance としても定義されています。
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User json
    name     Text
    age      Int
    deriving Eq Show Generic
|]
API
/users に GET で全ユーザ一覧を取得し、
/users/(name)/(age) に POST でユーザを作成します。
type HelloAPI  = "users" :> Get '[JSON] [User]
            :<|> "users" :> Capture "name" Text :> Capture "age" Int :> Post '[JSON] ()
Database
この例では persistent-mysql を使います。
runDB :: ConnectInfo -> SqlPersistT (ResourceT (NoLoggingT IO)) a -> IO a
runDB info = runNoLoggingT . runResourceT . withMySQLConn info . runSqlConn
doMigration :: IO ()
doMigration = runNoLoggingT $ runResourceT $ withMySQLConn connInfo $ runReaderT $ runMigration migrateAll
selectUsers :: IO [User]
selectUsers = do
    userList <- runDB connInfo $ selectList [] []
    return $ map (\(Entity _ u) -> u) userList
insertUser :: User -> IO ()
insertUser = runDB connInfo . insert_
runDB はクエリを投げるアクションを生成する関数です。
selectUsers でユーザ一覧をデータベースから取得します。
insertUser で新規ユーザをデータベースに作成します。
doMigration は初期テーブルを作成するアクションです。
Server
サーバ機能を実装します。
server :: Server HelloAPI
server = getUsers :<|> postUser
    where
        getUsers = lift selectUsers
        postUser n a = lift $ insertUser (User n a)
selectUsers :: IO [User] から getUsers :: ExceptT ServantErr IO [User] に変換するために lift を使っています。
Main
第一引数に 'migrate' をつけたら migration を実行するようにします。
main :: IO ()
main = do
    args <- getArgs
    let arg1 = if (length args > 0) then Just (args !! 0) else Nothing
    case arg1 of
        Just "migrate" -> doMigration
        _ -> run 8080 app
Dependencies
特に persistent 関連で多くのライブラリに依存しています。
import と language extensions を書き出しておきます。
{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}
module Main where
import           Control.Monad.Logger         (NoLoggingT (..))
import           Control.Monad.Trans.Class    (lift)
import           Control.Monad.Trans.Reader   (runReaderT)
import           Control.Monad.Trans.Resource (ResourceT, runResourceT)
import           Data.Aeson
import           Data.Text                    (Text)
import qualified Data.Text                    as T
import qualified Data.Text.IO                 as T
import           Database.Persist
import           Database.Persist.MySQL       (ConnectInfo (..),
                                               SqlBackend (..),
                                               defaultConnectInfo, runMigration,
                                               runSqlPool, withMySQLConn)
import           Database.Persist.Sql         (SqlPersistT, runSqlConn)
import           Database.Persist.TH          (mkMigrate, mkPersist,
                                               persistLowerCase, share,
                                               sqlSettings)
import           GHC.Generics
import           Network.Wai
import           Network.Wai.Handler.Warp
import           Servant
import           Servant.API
import           System.Environment           (getArgs)
executable persistent
  hs-source-dirs:      persistent
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , text
                     , either
                     , aeson
                     , wai
                     , warp
                     , mtl
                     , monad-logger
                     , resourcet
                     , transformers
                     , persistent
                     , persistent-mysql
                     , persistent-template
                     , servant
                     , servant-server
                     , haskell-servant-cookbook
  default-language:    Haskell2010
使い方
早速サンプルを動かしてみましょう。
mysql server が localhost:3306 で動作しているものとします。
セットアップ
- build
 
stack build
- create database
 
mysql -u test -e 'create database' -psecret servant_persistent
- migrate
 
stack exec persistent migrate
- test migration
 
mysql -u test -e 'show tables' -psecret servant_persistent
user table が作成されていればOK
5. serve
stack exec persistent
動作確認
以下のコマンドで動作を確認します。
$ curl -X POST http://localhost:8080/users/foo/12
[]
$ curl http://localhost:8080/users
[{"age":12,"name":"foo"}]
ユーザを作成して一覧取得できたことが確認できました。
参考文献

