Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (2) Persistent

# はじめに
先の記事で書いたServant内でPersistent/EsqueletoにてRDBにアクセスする機能を付け加えるのですが、その前に、Persistentのみに焦点をあてて記事を書きます。ポイントは、「Persistentはどういうものか」と「Persistentをどう動かすか」になります。

# Persistentとは
Persistentは「WebフレームワークYesodに含まれる、型安全なデータ永続化の仕組み」とのことです（[公式サイト](https://www.yesodweb.com/book/persistent)をざっくり意訳）。「Yesodに含まれる」とはいっても、Yesodとは独立に利用できるため、Yesodは気にしなくても構いません。

WebAPI向けに利用する側からすると「型安全にRDBにアクセスできるORM」といったところです。ORMは平たくいうと「RDBのデータとプログラミング言語の枠組み（変数であったり型であったり構造体/クラスであったり）とを自由に行き来できる仕組み」ですね。特徴をいくつかピックアップすると

- データ型（オブジェクト指向言語でいう「クラス」）を定義すると、DBからとってきたデータが定義した型として扱える／型として保持しているデータをDBに保存できる

これは当たり前の機能ですね。さらにNull可のメンバはHaskellではMaybe型として扱われるため、DBのデータの取扱いもNull安全。OuterJoinした結果も同様にNull安全に取り扱えます（このあたりは別記事で解説します）

- DBへのアクセスとなるSQL生成の部分も型チェックの対象となる

DBのテーブルのIDは、DBレイヤとしては「Integer型」ですが、Haskellでは「各テーブルのID型」となります。personテーブルのIDはPersonId型、blogテーブルのIDはBlogId型となり、全く別の型として扱われます。SQL生成部分のコードも「どのID型で何にアクセスするか」が全て型チェックの対象となるため、実装時点で（＝実行する前から）かなり安全なコードになります。もし、SQL生成部のコードにミスがあればビルドエラーとなります。ビルドが通った時点で「つじつまが合わないコードが含まれない」ため、RDBを扱うアプリでありながら、「一発で期待した動きをしてくれる」こともめずらしくありません。

- オートマイグレーション機能

DBのテーブル定義をHaskellでのデータ型の定義に「半自動」で合わせてくれる機能があります。「半自動」ということは「半手動」でもあるわけですが、手動部分については「どうすればいいか」の指示に従えばOKです（どれくらいの範囲が自動かについては、後述）。

テーブル定義が半自動で対応してもらうのもありがたいですが、併せて「テーブル定義の別管理」が不要になるため、運用面で非常に楽ができます。

# テーブル定義

私は今回の記事シリーズのように「HaskellでRDB操作をしたい→そのためにはデータ型での定義が必要→データ型ってどう扱う？」という流れできましたので、ここでも「Haskellのデータ型とは」「RDBで使うためには」について簡単に記載してみます。

## Haskellデータ型、レコード構文

ここでいう「データ型」というのは、他のプログラミング言語でいう「構造体」のようなものです。オブジェクト指向における「クラス」に近い側面もあります。data宣言でデータ型の名前とフィールドを定義します。

```haskell
data Person = Person
  { name :: Text
  , age :: Maybe Int
  }
```

このようにPerson型とフィールドname, ageを定義すると、Person型の値からフィールドnameを取り出す関数「name」が自動的に生成されます。他のプログラミング言語でいう「ゲッター関数（メソッド）」に相当します。他のプログラミング言語でのゲッターメソッドは、クラスや構造体それぞれの名前空間で定義されるのですが、Haskellではグローバルな関数として扱われます。そのため、他の型の関数と名前がかぶってはいけません（名前の衝突を許容するやり方はあるようですが、ここでは省略します）。データ型の名前をプレフィックスにつけて、名前の衝突を防ぐ、というやり方もあるかと思います。

```haskell
data Person = Person
  { personName :: Text
  , personAge :: Maybe Int
  }
```

こうすると、Person型からnameを取り出すゲッター関数は「personName」となります。

## RDB向けのデータ型（テーブル）定義

RDBアクセス用にデータ型を定義する場合、「TemplateHaskell」という仕組みを使うため、記述法が変わります（data宣言がない、ブレースがない、フィールド名と型の間のコロンがない、IntとMaybeの順が逆、など）。

```haskell
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person
    name Text
    age Int Maybe
|]
```

このように定義すると、Person型が定義され、なおかつRDBのテーブルにアクセスできるようになります。上記の「データ型」と同様、ゲッター相当の関数も自動生成されますが、型の名前の先頭を小文字にしたもの（上記の例では「person」）がプレフィックスにつく点が異なります。このゲッター関数は、Haskell上ではキャメルケース、DB上ではスネークケースになります。上記の例では、ゲッター関数は「personName」「personAge」となります。

## 列挙型の扱いについて
各データの属性値を管理したい場合、例えば「personテーブルのレコードで、user用レコードならtype=1, admin用レコードならtype=2」としたいようなケースがあるかと思います。例えば、属性定義のマスターテーブルを作成して、結合させながら使う、というのもあるかと思いますが、Haskellでは、列挙型として値を取り扱うのがオススメです。

先の例の「personテーブル」でいうと、下記のように「PersonType」という型を定義し、その値（PersonTypeUser あるいは PersonTypeAdmin）をpersonテーブルのtypeカラムに設定します。derivePersistField関数を使用すると、その型をPersistentでの型として使用できるようになります。属性管理専用のテーブルは不要ですので、DB構成がすっきりします。

```haskell
-- 列挙型定義
data PersonType = PersonTypeUser | PersonTypeAdmin
    deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "PersonType" -- Persistentで使用するために必要

-- モデル定義
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  Person
    name Text
    age Int Maybe
    type PersonType -- PersonTypeという型が直接使える
|]
```
このように定義した型の対はDBではどのようになるか？ですが、これはMySQLではText型として設定されます。Haskellで「PersonTypeUser」という値の場合、MySQLでは"PersonTypeUser"という文字列して保存されます。Haskellのプログラムがレコードを作成する限り、型に定義されている値（＝文字列）しか保存されないですが、Haskellプログラムが変更になったり、手動でデータを操作したりした場合には、型に定義されていない値が入ることになり、その後にHaskellでデータを読み出そうとした場合には、実行時エラーが発生します。開発時に起こりがちですので、注意が必要です。

ところで、こういった属性値のような「有限の種類の値」をRDBで扱う場合に、RDB側での型を「文字列型」にした場合と「整数型」にした場合で、どれくらいパフォーマンスに違いがあるのか？というところが気になりまして、ざっくり調べてみたのですが、なかなかよくわかりませんでした。話題がみつからなかった、というのは、「特に問題ない」ということなのか...なんともいえないです。

# 使い方
## 接続設定（MySQL）
この節のみMySQL限定です。PostgreSQL等、他のDBでは接続設定のデータ型が異なりますが、ドキュメントを見れば、簡単に対応できると思います。

```haskell
myAppConnectInfo :: ConnectInfo
myAppConnectInfo = ConnectInfo
    { connectHost     = "localhost"
    , connectPort     = 3306
    , connectUser     = "root"
    , connectPassword = ""
    , connectDatabase = "persist_test"
    , connectOptions  = [InitCommand "SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY';"
                       , InitCommand "SET NAMES utf8;"
                       ]
    , connectPath     = ""
    , connectSSL      = Nothing
    }
```

ほとんどの項目は特に迷う要素はないと思いますが、connectOptionsについては以下の点に留意ください。Haskellというよりは、一般的なMySQLクライアント設定です。

- 「SET SESSION」の引数は、「TRADITIONAL」を設定するなど、安全サイドにふっています（あいまいな記述を認めない、など）
- 「SET NAMES utf8」は、これがないとLinuxにて日本語が文字化けしたために入れています

## SQL実行

ここでは簡単のため、main配下で実行する、という前提とします。Servantのハンドラでの実行は、別の記事で記載します。

persistentパッケージには、様々なSQL実行スタイルが用意されていますが、大まかな分類としては、「接続設定→DBとのコネクション作成→SQL実行」と「接続設定→DBとのコネクションプール作成→SQL実行」というパターンがあります。前者の方が若干簡単なので、ここでは前者のパターンを記載します。いずれのパターンであっても、SQL実行の中身についての影響はありません。

runSqlを下記のように定義します。

```haskell
runSql :: SqlPersistT (ResourceT (LoggingT IO)) a -> IO a
runSql = runStdoutLoggingT . runResourceT . withMySQLConn connectInfo . runSqlConn
```

そうすると、main中では、このrunSqlに続くdoブロックでSQLを実行できます。SQLが１行であれば、doはなくても構いません。本記事でのrunSqlは、IOモナドの型となっていますので、main中でそのまま利用できます。

以下では、CRUDのうち、C（Create）とR（Read）について記述します。

## Create

CRUDのC（Create）です。SQLと同様「insert」という関数を使います。insertは戻り値として挿入されたレコードのIDが返ります。以下の例はどちらも、見かけ上は同じ動作をします。どちらのスタイルがいいかについては、トランザクションの扱い等によって決まりますが、基本的にはスタイル1の形を多用するかと思います。

```haskell
main = do
  -- 実装スタイルその1
  runSql $ do
    pid <- insert $ Person {personName = "Tom", personAge = Just 25}
    liftIO $ print pid

  -- 実装スタイルその2
   pid <- runSql $ insert $ Person {personName = "Tom", personAge = Just 25}
   print pid
```

## Read

CRUDのR（Read）です。本記事ではpersistentのget関数を例に挙げます。本格的なReadをする場合には、SQLでおなじみの「select」を使いますが、「単純にレコードのIDからレコードをひきたいだけ」といったような場合はgetが簡単で良いです。selectについては、次の記事で記載します。

```haskell
main = do
  runSql $ do
    let pid :: PersonId = toSqlKey 1
    p <- get pid
    liftIO $ print pid
```

「get関数を実行すると、どのテーブルにアクセスするのか」は、getの引数で指定するIDの型によって決まります。Haskellとしては、IDの型はInt等ではなく、テーブル毎のIDとして違う型として扱われるため、型からテーブルを「推論」することができます。
例にある「toSqlKey関数」は、Int型（正確にはInt64型）からテーブルのIDの型に変換する関数です。pidの型を「PersonId」と指定しているため、ここでのgetは「personテーブルにアクセスすればいいんだな」と推論します。この型の指定を外すと、ビルドエラーとなります。

DBには、get関数に指定したIDをもつレコードがある場合と、ない場合があります。その両方のケースを扱うため、get関数を実行して取得できる値（上記の例では「p」に入っている値）は、Maybe型となります。上記の例では「Maybe Person」という型です。値がある場合には、printの結果として「Just （Personレコードの中身）」が、ない場合には「Nothing」と表示されます。

今回は、Maybe型をひっくるめてprintしていますが、「値がある場合にその値を使って何かをする」ときには、caseを使った場合分けなどの実装が必要です。

```haskell
main = do
  runSql $ do
    let pid = toSqlKey 1
    p <- get pid -- ここでビルドエラー発生
    liftIO $ print pid
```

selectを使ったレコードの取得は次の記事で記載します。　

## オートマイグレーション

```haskell
doMigration :: IO ()
doMigration = runNoLoggingT . runResourceT . withMySQLConn connectInfo . runReaderT . runMigration $ migrateAll
```
このような関数を定義しておくと、main中に「doMigration」と書くだけで、上述した「半自動のマイグレーション」が走ります。ここにある「migrateAll」は、テーブル定義のshareの行に書いてある「"migrateAll"」です。

自動／手動の区分は概ね下記の通りです。

- 自動で動く
  - テーブル新規作成
  - カラム追加（ただし、デフォルト値の対応は含まれない）
  - 外部キー制約設定、プライマリーキー設定、オートインクリメント設定など

- 自動では動かず、エラーメッセージが出て終了する（＝手動での対応が必要なもの）
  - カラム削除

- エラーメッセージも出ない（＝対応してもいいが、しなくてもいいもの）
 - テーブル削除

不要になったテーブルは、自動では削除されませんが、あっても問題はないためか、「削除してください」というようなエラーメッセージも出ません。もちろん、いらないものは消したほうがいいかとは思いますので、手動でdrop tableしましょう。

カラム名変更は、Persistentとしては「カラム削除＋カラム追加」として扱われます。当然自動の対応はありません。該当するカラムにデータが存在する場合には、マイグレーションを実行する前にalter table等で手動対応する方が手間は少ないと思います。

カラム追加の際に、レコードが既に存在する場合でも、そのレコードに対するデフォルト値の設定等はありません。そのような際には、マイグレーション後に手動で対応する必要があります。

# まとめ

本記事では、Servantを含まない形でPersistentを使用することについて、概要を書いてみました。いろんなことを書いていますが、書いてない話の方が圧倒的に多いです。ここでかいていないテーマのうち、WebAPIとして使いがちなテクニックについては、後の記事で書いていく予定です。