Haskell
WebAPI
persistent
servant

Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (4) ServantとPersistent/Esqueletoを結合する

はじめに

これまで、Servant、Persistent、Esqueletoをそれぞれ説明してきました。本記事では、これらを結合し、「Servantのハンドラ内でPersistent/Esqueletoが使える」ようにします。以前に別の記事で同じような内容を書いていますが、本記事は実用WebAPIシリーズの一記事として書き直しています。

Servantバージョンについての注意

2017年12月時点でのServantバージョンは0.12です。少なくとも、Servantのバージョン0.9→0.11→0.12に至るに従い、本記事で触れる「HandlerモナドにReaderTを適用」についてのBreaking changesが続いています。現時点では、Stackageに登録されているServantの最新バージョン0.11まで適用できる方法について記載します。恐らく、バージョン0.12(以降)がLTSに登録されることになり、その際にはこの記事の内容はそのままでは使えないことになるはずですので、Servantを使用する場合には、LTSバージョン等に注意する必要があります。

結合実装・ステップ1(単純結合)

先の記事で、runSqlというIO関数を定義しました。一方、Servantのハンドラもモナド変換子がついていますが、もとはIOですので、liftIOを使えば、IO関数を利用できます。ですので、下記のように、APIハンドラ内でliftIOを使ってrunSqlでのDBアクセスは可能です。

getPersonHandler :: PersonId -> Handler Person
getPersonHandler pid = do
  liftIO $ runSql $ do -- liftIO + runSqlでDBアクセス
    p <- get pid
    case p of -- pはMaybe Person型
      Just person -> return person -- 該当PersonIDがあったので、データを返す
      Nothing -> throwError err404 -- 該当PersonIDがない場合に404エラーを返す

可能ではあるのですが、このような実装には以下のような問題があります。

  • APIハンドラが動作するたび(=APIリクエストが発生するたび)に、ハンドラ関数内でDBに接続/切断を行う。この処理は無駄なオーバーヘッドとなる。開発時はあまり問題にはならないが、本稼働時に負荷耐性を落としてしまう。
  • 接続先DBがビルド時に決まってしまう。アプリ実行時に設定で変える、とはできない。開発時に、いろんな環境での動作をさせるのが難しい

APIハンドラでのDB接続をしないようにするには、WebAPIサーバ起動時にDBコネクションプールを作成して、それをAPIハンドラで使いまわせばOKです。また、DBコネクションプールを作る時に接続するDBを選択することもできます(詳細は後の記事で)。そのため、この問題を解決するステップは下記のようになります。

  • プログラム開始時に、コマンドラインオプションや設定ファイルから設定のセットを作成する
  • Webサーバ起動時に、設定のセットに基づきDBとのコネクションプールを作成する
  • APIハンドラに何らかの形で、コネクションプールを渡す
  • APIハンドラでは、コネクションプールを使ってDBアクセスをする

この中で、一番の問題は「どうやってAPIハンドラにコネクションプールを渡すか」となります。一般的なプログラミング言語では割とハードルの低い問題ですが、Haskellではそれなりの「仕込み」が必要になります。

グローバル変数相当の機能について考える

Haskellでの話はちょっと横においておいて、他のプログラミング言語ではどうするかを考えてみましょう。たいていのプログラミング言語では「グローバル変数」があり、それはAPIハンドラ関数の内部からも参照できますので、ハンドラ内部でそれを参照して動作を制御できます。今回の課題であれば、このグローバル変数にコネクションプールを保持して、APIハンドラから参照すればOKですね。グローバル変数は「変数」なので、ハンドラから「参照だけ」と「参照&更新」のどちらも可能ですが、今回の課題では、ハンドラからの参照ができればいいので、以下では「参照だけ」に限定するとします。

ここでHaskellの話に戻ります。Haskellではそもそも「変数」がありません。そのため「作成した情報を別の関数内で参照できるようにする」あるいは「状態を保持/更新/参照できるようにする」ためには、モナドを利用することになります。今回のような「設定した情報を別の関数内で参照できる」ような仕組みとして、ReaderモナドやIORefなどがあります。あります…なのですが、例えばIORefでは、IORefを作成しただけではAPIハンドラからIORefにはアクセスできません。なんらかの形でIORefハンドラをAPIハンドラに「渡して」あげないといけません。APIハンドラの引数は(大まかにいえば)Servantで決められてしまうため、「IORefハンドラを引数に追加する」等はできません。Readerモナドも基本的には同じような事情です。

Servantでは、APIハンドラの引数はいじれないですが、ハンドラで扱うモナドは変更できます。Servantでの「参照用グローバル値」はハンドラでのモナドにReaderTを適用することで実装できます。

結合実装・ステップ2:(ハンドラの型を変換)

狙い

WebAPIの開発から本稼働までを考えると、上述したコネクションプール以外にも、APIハンドラで参照したい値がいろいろと存在します。デバッグモードのフラグだったり、接続先のDBの指定であったり、です。こういった情報も含めて「MyAppConfig」という型として定義し、WebAPI開始時に値を設定し、ハンドラ内で読み出しができるようにできればいいわけです。先の記事で書いたAPIハンドラ用のHandlerモナドにrunReaderTをかぶせることができれば、ハンドラ内でMyAppConfigの値にアクセスできるようになります。

MyAppConfig型

以下では、自分で定義する型や関数がわかりやすいように、「MyApp」というプレフィックスをつけています。

APIハンドラで参照できる「グローバル値」用のコンテナとして、MyAppConfig型を作成します。この型は好きなメンバを定義できますが、ここでは例として下記のデータ型とします。

data MyAppConfig = MyAppConfig
    { getPool :: ConnectionPool -- DBアクセス用プール
    , getApplicationText :: Text -- アプリ設定テキスト
    , getApplicationFlag :: Bool -- アプリ設定フラグ
    }

MyAppHandler型定義

準備ができたので、APIハンドラ用モナドであるHandlerにReaderTを適用してMyAppHandler型を作成します。これを用いて、ハンドラの型がhandlerHoge :: 引数の型 -> Handler 戻り値型となっていたところを、handlerHoge :: 引数の型 -> MyAppHandler 戻り値の型となるようにします。

type MyAppHandler = ReaderT MyAppConfig Handler

APIハンドラ登録、アプリ定義

「enter」と「runReaderTNat」を使って、Server APIおよびApplicationを作成します。ハンドラ登録では、MyAppHandler型からMyAppServer型を生成し、これを以前の記事の「Server型」と置き換えます。

-- APIハンドラ登録
type MyAppServer api = ServerT api MyAppHandler

-- アプリ定義
myAppApp :: MyAppConfig -> Application
myAppApp = serve myAppApi . myAppToServer

myAppToServer :: MyAppConfig -> Server MyAppAPI
myAppToServer cfg = enter (runReaderTNat cfg :: MyAppHandler :~> Handler) myAppServer

アプリ実行

MyAppConfigの値を作成し、appの引数に与えてrun(実行)します。本記事でのmyAppAppはMyAppConfig型の値(ソース中の「cfg」)を引数にとりますので、このcfgの値でAPIの動作を設定することができます。

  let pool_size = 8
  pool <- runNoLoggingT $ createMySQLPool connect_info pool_size
  let port = 3001
      cfg = MyAppConfig {getPool = pool, getApplicationText = "Fugafuga", getApplicationFlag = True}
  run port $ myAppApp cfg

APIハンドラ内でのDBアクセス

APIハンドラでpoolにアクセスできる環境ができましたので、それを利用してDBアクセスできるようになります。runReaderTを仕込んだものは、ハンドラ内ではasks関数で取得できます。コネクションプールは「getPool」に仕込んでますので、これを使ってrunSqlを修正すると、下記のようになります。

type SqlPersistM' = SqlPersistT (ResourceT IO)

runSql :: (MonadReader Config m, MonadIO m) => SqlPersistM' b -> m b
runSql query = do
    pool <- asks getPool
    liftIO $ runResourceT $ runSqlPool query pool

修正したrunSqlですが、runSql以下の書き方は変わりません。runSql直後のdo配下のブロック内では、1つのトランザクションとして扱われます。

getPersonList :: Maybe PersonType -> MyAppHandler [ApiPerson]
getPersonList ptype = runSql $ do
  plist <- select $ from $ \p -> do
    where_ (if M.isNothing ptype then val True else p ^. PersonType ==. val (fromJust ptype))
    return p
 ...以下略

なお、getPool以外の値もハンドラ内でアクセスできますが、詳細については後の記事で記載予定です。

まとめ

WebAPIを実装する時に、ここで書いたような実装で十分なこともあれば、さらにアレンジが必要なことあります。アレンジをするには「なぜこうなっているのか」についての理解が必要となります。

理解の助けになるように、説明は長くなってしまいましたが、今回書いたようなモナドの型変換は、ほぼ全てが機械的なものです。実装そのものも簡単にできるでしょうし、テンプレートをベースに機能を実装していくのもいいかと思います。

ここまでの実装を含んだサンプルコードをテンプレートとして使えるように、Githubに上げています。このリポジトリは、まだここで書いてない機能や実装も含んでいますが、以後の記事で触れていく予定です。
https://github.com/cyclone-t/servant-esqueleto-sample