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

# はじめに

前記事ではPersistentを単独で扱いましたが、今回はそのPersistentの拡張としてEsqueletoを扱います。Esqueleto自体はjoinを中心に多くの別記事を書く予定ですので、ここではイントロレベルだけにとどめておきます。

# Esqueletoとは

Persistentの拡張として、Persistentと同じ枠組みで、より多彩なSQLを構築できるモジュールです。[公式サイト](https://hackage.haskell.org/package/esqueleto)の説明もそんな感じですね。Persistentで扱えるSQLはかなり限られているのですが、かわりにEsqueletoを使うことで、各種のRDBで使いがちなSQLの多くを利用できます。もちろん、全てが利用できるわけではないですので、「RDBの機能をフルに使い切る」というよりは「現実的なSQLを便利に扱える」といった狙いになります。

Persistentだけだとjoinを含んだSQLが作成できないため、実用WebAPIを作ろうした時にはかなり辛いところです。そもそも「RDB」を使っておきながら、joinを全く使わずにアプリを作ろう、という考えがどうか、というところもありますけどね。それはさておき、RDBアクセスのベースとしてはPersistentをベースにしつつ、Esqueletoをメインに活用する形がオススメでしょう。

Webフレームワークの中には自動SQLビルダが標準搭載されているものもありますが、Esqueletoは「手動SQLビルダ」相当です。先に生成したいSQL文を考えてからHaskellでの実装となります。そのため、SQLに関する知識は必要です。Esqueletoを使うメリットとしては、コンパイルが通っている時点で、かなり高い確率で「構文的にOKなSQL」となります。いろんな事情で100%ではないのですが、それでも型安全の仕組みは強力です。

# 使い方

Haskellのソースでは、`import Database.Esqueleto`とすればOKです。テーブル定義やDBとの接続設定はPersistentのものを利用します。また、Persistentでの主要関数はEsqueletoで再エクスポートしていますので、多くの場面で```import Database.Persist```は不要となります。

# SQL操作

以下では、前記事でのCRUD操作のうち、Rの後半以降を扱います。

## Read

前記事の`get pid`をEsqueletoで書くと、下記のようになります。

```haskell
runSql $ do
  let pid :: PersonId = toSqlKey 1
  p_record <- select $ from $ \p -> do
    where_ $ p ^. PersonId ==. val pid
    return p
  liftIO $ print $ entityVal <$> p_record
```

見た目がgetよりは複雑になっていますが、拡張性が高いのでしょうがない話です。とはいえ、DSL自体がSQLの構文に寄っていますので、覚えるハードルはそんなに高くないかと思います。「where_」は末尾に「\_（アンダースコア）」がついていますが、これはHaskellの予約後に「where」があるためです。同じくSQLのinは、Esqueletoでは「in_」と定義されています。

`select from`の後ろにテーブル名がないですが、これもgetの時と同様、from以下に含まれる内容によって型推論→対象のテーブルが決まる、となります。fromの後ろはLambda式になっていまして、検索条件＋戻り値を指定した関数をセットします。テーブル名を明記しないのは、selectに限らず、後述するupdateやdeleteでも同じです。

select関数の一般的な使い方は下記のようになります。

```haskell
runSql $ do
  p_record <- select $ from $ \p -> do
    -- ここにjoin、where、orderBy、limitなどが入る
    return p
  -- p_recordを使ったアクション等をここに実装
```

fromの後ろのpは、Lambda式の仮引数ですので、名前は何でも構いません。どういう名前になっても、型推論（その結果としての、アクセスするテーブル指定）には影響しません。前記事のget関数とは異なり、selectの結果として取得できるレコードの件数は0の場合もあれば複数の場合もあります。そのため、selectの実行によって取得できる値は、最後の「return p」のpのEntity型のリストとなります。上記の例でいうと、pの型はPersonのため、selectの結果は、[Entity Person]型（Entity Personのリスト型）になります。Entity型については次節に記述します。join, where等の詳細は後の記事で解説予定です。

## selectの戻り値の取り回し

selectの戻り値の型に使用されるEntity型は「レコードのIDと（ID以外の）レコードの値をペアにしたもの」です。Entity型はMaybe型と同様に、他の型を引数にとります。上記の例では「Entity Person型」となります。selectの戻り値はEntity型のリストですので、取得した値の取り回しは「リストからの取り出し」と「Entity型からの取り出し」を順に行う必要があります。

Entity型からの取り出しは、「関数を使って取り出す」方法と、「コンストラクタを使って取り出す方法」があります。まずは関数を使って取り出す方法です。entityKeyはEntity型からIDを取り出す関数、entityValは同じくレコードを取り出す関数です。最後の行の「error」ですが、ID（＝プライマリーキー）をキーにして検索しているので、複数レコードが戻ってくることは「あり得ない」状況です。そのようなケースに対しては「error」と実装しておくと、「起こり得ない」というのを明示できる、かと思います。今回のケースでは起こり得ないですが、もしこのerrorのケースにマッチした場合は、実行時に例外が投げられます。例外への対処をしないと、プログラムが終了しますが、対処することも可能です。例外対処は別記事で記載します。

```haskell
case p_record of
  [entity_p] -> do
    let pid = entityKey entity_p
        p = entityVal entity_p
    liftIO $ print $ "pid=" ++ show pid ++ ", p=" ++ show p
  []         -> liftIO $ print "No data"
  _          -> error "Multiple data for primary key"
```

次に、コンストラクタを使ったパターンです。パターンマッチや関数の引数にコンストラクタ（「Entity pid p」の部分）を記述すると、IDとレコードがそれぞれpidとpにセットされた状態になります。コンストラクタが使えるケースでは、こちらのスタイルの方が記述が若干コンパクトになります。

```haskell
case p_record of
  [Entity pid p] -> liftIO $ print $ "pid=" ++ show pid ++ ", p=" ++ show p
  []             -> liftIO $ print "No data"
  _              -> error "Multiple data for primary key"
```


## Update

CRUDのU（Update）です。Esqueletoでのupdateの例は下記のようになります。pid作成やrunSqlは省略しています。

```haskell
update $ \p -> do
  set p [PersonName =. val "Peter", PersonAge =. val (Just 30)]
  where_ $ p ^. PersionId ==. val pid
```

これもSQLの構文に近いので、解説はほぼ不要かと思います。値は何も返しません。Updateがうまくいったかどうかは、2通りの確認の仕方があります。

- updateのかわりにupdateCountを使う

updateの結果、何件のレコードが更新されたかが返ります。これはwhere等の条件にマッチした件数ではなく、値が本当に変更された件数ですので、値が既存のレコードと同じ場合には、0が返ります。

- updateが投げる例外を受け取る

updateできない状況（Unique制約、外部キー制約など）の場合に投げられる例外を受け取ります

## Delete

最後はCRUDのD（Delete）です。

```haskell
delete $ from $ \p -> where_ $ p ^. PersonId ==. val pid
```

Deleteは1行で済みがちなので、doを使わずに書いてみました。
もちろん、doを使って書いてもビルドや実行は問題ありません（Lintに「不要なdoがある」と文句を言われますが）。

deleteが返す値は、updateと同様です。deleteにかわるものとして、deleteCountがあるのも同じです。

# 記述をシンプルにするためのTips

EsqueletoはSQLビルダ向けのDSLということで、独自の演算子を多く使用します。importで`import Database.Esqueleto as E`とやって、各演算子に「E.」のプレフィクスをつけてもいいのですが、見た目もごちゃごちゃしますし、コーディングもだるいです。

```haskell
p_record <- E.select $ E.from $ \p -> do
  E.where_ $ p E.^. PersonId E.==. E.val pid
  return p
```

↑うざいですよね？

ですので、他のモジュールと重複する関数だけは、「E.」をつけますが、他の関数にはプレフィックスはつけないのがいいかと思います。他のモジュールと衝突しがちなのは、EsqueletoのisNothingとData.MaybeのisNothing、です。こういう場合には

```haskell
p_no_age <- select $ from $ \p -> do
  where_ $ E.isNothing (p ^. PersonAge)
  return p
```

というように、「E.isNothing」と表記します。

あと、Esqueletoで定義されている演算子の多くは、末尾にピリオドが着いています。これを忘れるとEsqueletoではないモジュール扱いになり、ビルドエラーになるので気をつけましょう（慣れればミスも出なくなりますし、仮にミスをしてもビルドエラーが出ますので、問題にはなりません）。

# まとめ

Esqueletoについてイントロレベルで記載しました。Esqueletoは利用できる関数の数自体も多く、さらにその使い方のバリエーションとなると、膨大な量になります。そのバリエーションの中でも「Haskellというよりは、SQLレベルの話題」であったり、「WebAPIそのものとは直接の関わりはない」というようなものも多いですが、「実用WebAPIを実装する時には必要となるもの」という観点で記事を続けていく予定です。