はじめに
前記事ではPersistentを単独で扱いましたが、今回はそのPersistentの拡張としてEsqueletoを扱います。Esqueleto自体はjoinを中心に多くの別記事を書く予定ですので、ここではイントロレベルだけにとどめておきます。
Esqueletoとは
Persistentの拡張として、Persistentと同じ枠組みで、より多彩なSQLを構築できるモジュールです。公式サイトの説明もそんな感じですね。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で書くと、下記のようになります。
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関数の一般的な使い方は下記のようになります。
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のケースにマッチした場合は、実行時に例外が投げられます。例外への対処をしないと、プログラムが終了しますが、対処することも可能です。例外対処は別記事で記載します。
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にセットされた状態になります。コンストラクタが使えるケースでは、こちらのスタイルの方が記述が若干コンパクトになります。
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は省略しています。
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)です。
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.」のプレフィクスをつけてもいいのですが、見た目もごちゃごちゃしますし、コーディングもだるいです。
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、です。こういう場合には
p_no_age <- select $ from $ \p -> do
where_ $ E.isNothing (p ^. PersonAge)
return p
というように、「E.isNothing」と表記します。
あと、Esqueletoで定義されている演算子の多くは、末尾にピリオドが着いています。これを忘れるとEsqueletoではないモジュール扱いになり、ビルドエラーになるので気をつけましょう(慣れればミスも出なくなりますし、仮にミスをしてもビルドエラーが出ますので、問題にはなりません)。
まとめ
Esqueletoについてイントロレベルで記載しました。Esqueletoは利用できる関数の数自体も多く、さらにその使い方のバリエーションとなると、膨大な量になります。そのバリエーションの中でも「Haskellというよりは、SQLレベルの話題」であったり、「WebAPIそのものとは直接の関わりはない」というようなものも多いですが、「実用WebAPIを実装する時には必要となるもの」という観点で記事を続けていく予定です。