LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-29

はじめに

前記事では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を実装する時には必要となるもの」という観点で記事を続けていく予定です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1