Haskell
RDB
esqueleto

Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (11) Esqueleto:selectの基本

More than 1 year has passed since last update.


はじめに

WebAPIとしての最低限の枠組みができてきたので、このあたりでRDBアクセスの本命である「Esqueleto」に話題を移します。WebAPI自体の話題もまだ残りがあるのですが、それはEsqueletoの話を一通りした後に再開します。ここからしばらく続くEsqueletoの記事は、ほぼ純粋に「RDBアクセスだけ」に限った話でありWebAPIとは独立といえば独立です。ですがDBを使ったWebAPIの実装の大半がこのRDBアクセスとなるため、Esqueletoに集中して記事を書きます。Esqueletoは型安全の仕組みがすごい反面、使い方に慣れるにはそれなりのハードルもあるため、丁寧に書いていこうと思います。

Esqueletoの記事はの1本目は「selectの基本」となります。select系の記事はJoinの話も含めると全部で5本くらいになりますが、今回はJoinを含まない、selectそのものの話とselectの結果取得できるEntity型の話となります。


select関数の基本的な使い方

本記事のメインテーマのselectです。Esqueletoの紹介記事でも書きましたが、基本的な作りはこんなところになります。runSql等は省略しています。

 -- selectした結果が「eplist」に入る

eplist <- select $ from $ \p -> do -- select, from, 関数(doブロック含む)でワンセット
-- join, on, where_, limit, orderByなど
return p -- returnで閉じる

これも以前の記事に書いたのですが、Esqueletoのselectには「SELECTする対象のテーブルを指定」する引数等はありません。上記の「join, on...」と書いたところで推定される型により、テーブルが決まります。そのため、何も条件を書かなければ「型が決まらない」というコンパイルエラーになります。

  -- これだけだとコンパイルエラーになる 

eplist <- select $ from $ \p -> do
return p

もしpersonテーブルの全レコードを取得したいのであれば、SQLとしては「SELECT * FROM person」となりますよね。そういうSQLを発行したい場面もあるでしょうから、その方法について説明します。要は上記のコードのp、あるいは戻り値が入る「eplist(名前はEntity Personのリストという意図です)」の型がPersonに紐付けばいいわけです。後者のアプローチでいくと、eplistの型は「[Entity Person]」になりますから、それを型指定します。関数の内部で使う変数の型指定をする場合はGHC拡張「ScopedTypeVariables」が必要なので、ソースの冒頭に入れましょう。

-- 必要なGHC拡張

{-# LANGUAGE ScopedTypeVariables #-}

eplist::[Entity Person] <- select $ from $ \p -> do
return p

発行されるSQLは下記のようになります。SQLのデバッグ出力については、別の記事で記載します。Persistent、Esqueletoともに「SELECT * FROM..」のようなアスタリスクは使われず、SELECTの後ろには全てカラムが指定されます。

[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`

FROM `person`

現実的には何かしらの「whereでの条件指定」をすることになるでしょう。where_を追加すると、型が決まりますので、戻り値での型指定は不要です。こんな感じですね。where_の後ろの$を忘れると、型があわずにコンパイルエラーとなります。慣れないうちは忘れがちなので注意しましょう。

  eplist <- select $ from $ \p -> do

where_ $ p ^. PersonName ==. val "Tom"
return p

ここでも書きますが、「where」はHaskellでの予約語のため、Esquletoの関数としては「where_」となっています。

発行されるSQLは下記になります。SQL文のWHERE節で「?」の部分には、最下行の「"Tom"」が入ります。このように、SQLインジェクション対策はPersistent/Esqueleto側でやってくれるので、Haskell実装としての対策は不要です。

[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`

FROM `person`
WHERE `person`.`name` = ?
; [PersistText "Tom"]


WHERE節の書き方

Esqueletoでの最初のハードルが、「どうやってSQLでのWHERE節を書けるようになるか」です。理屈がどうというより、例をいっぱい見て慣れるしかないかと思います。

記述法の考え方は下記の通りです。発行したいSQLのWHERE節が「WHERE テーブル名.カラム名 = 値」であるとします。


  • 「テーブル名.カラム名」の部分は、fromの後ろの関数の仮引数pを使ってp ^. テーブル名カラム名となる。「^.」はEsqueletoで用意されている演算子

  • 「=」の部分は「==.」とする。これもEsqueletoで用意されている演算子

  • 「値」の部分は、即値(文字列であったり数値であったり)の場合は「val」をつける。これはEsqueletoで用意されている関数

説明としてはこんな感じになりますが、それよりもwhere_ $ p ^. PersonName ==. val "Tom"という見方に慣れましょう。SQLが「WHERE person.id = pid(←便宜的に変数で書きます)」であれば、Esqueletoではwhere_ $ p ^. PersonId ==. val pidです(pidはPersonId型とします)。最初のうちによくやるミスは「==.のピリオドを忘れる」「valを付け忘れる」です。どちらをやってもビルドエラーになります。ビルドエラーが出たら、こういったミスがないかを確認しましょう。


select戻り値の扱い方

select関数の戻り値は「[Entity a]型」になります。上の例なら「[Entity Person]型」ですね。Entity PersonはPersonIdとPersonをペアにしたものです。ので、それぞれを使いたい場合には下記の要領で分解します。


PersonIdのみ取得したい

Entity Person型の値からIDを取り出す関数はentityKey関数です。下の例では、「ep」にEntity Person型の値があるとしています。繰り返しますが、selectの戻り値はEntity aの「リスト」です。リストのままIDを抜き出すのであれば、fmapの演算子「<$>」を使って、このようになりますね。リストのまま処理をしているので、取得した型もPersonIdの「リスト」となります。

-- EntityからIDを取得する。pidの型はPersonId型となる

let pid = entityKey ep

-- EntityのリストからIDを取得する。pid_listの型は[PersonId]型となる
let pid_list = entityKey <$> eplist


Personのみ取得したい

Entity Person型の値からPerson型の値を取得するのはentityVal関数になります。使い方はentityKeyと同じです。

-- Entityから値を取得する。pの型はPerson型となる

let p = entityVal ep

-- Entityのリストから値のリストを取得する。p_listの型は[Person]型となる
let p_list = entityVal <$> eplist


PersonIdとPersonを両方取得したい

Entity Person型の値からIDと値の両方を一度に取得したいのであれば、コンストラクタを使う方法もあります。Entity Personの「リスト」で同じことをするには、ちょっと工夫が必要のなので、ここでは省略します(話が長くなってしまうため)。

-- EntityからID,値を取得する。pidの型はPersonId型に、pの型はPerson型となる

let Entity pid p = ep


APIの戻り値にするには

先の記事で定義した「toApiPersonFE関数」は、Entity Person型をApiPerson型に変換する関数ですので、selectの戻り値を直接ApiPerson型に変換することができます。

-- api_person_listは[ApiPerson]型

let api_person_list = toApiPersonFE <$> eplist


WHERE節の書き方(その2)

ここでは、WHERE節で書きがちな条件をいくつか紹介します。


IN

SQLでいう「WHERE person.id IN (1,2,5)」というような書き方です。Esqueletoでは「in_関数」を使います。inもHaskellの予約語なので、「in_」となっています。

使い方は、さっきの例の「==.」のかわりに「`in_`」を使います。関数を演算子のように使用するとSQLっぽく見えるようになるため、in_をバッククォートでくくっています。INの後ろに即値が来る場合、valではなくリスト用の「valList」を使います。

  -- デモ用にPersonIdのリストを作成

let pid_list::[PersonId] = toSqlKey <$> [1, 2, 5]

eplist <- select $ from $ \p -> do
where_ $ p ^. PersonId `in_` valList pid_list -- in_とvalListの使い方に注目
return p

発行されるSQLは下記の通りです。SQLではINになっていますね。

[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`

FROM `person`
WHERE `person`.`id` IN (?, ?, ?)
; [PersistInt64 1,PersistInt64 2,PersistInt64 5]


IS NULL

SQLでも「あるカラムがNULLであるレコードを検索する場合には『WHERE カラム名 IS NULL』としないといけない(「WHERE カラム名 = NULL」はアウト)」というハマリ(?)がありますが、Esqueletoには「IS NULL」に該当するisNothing関数があります。isNothing関数はData.Maybeモジュールにも同名の関数があり、名前かぶりになりがちので、Esqueletoであることを明示する方がいいでしょう。なお、Maybe型となっていないカラムに対して「isNothing」をしようとすると、型があわずにコンパイルエラーとなります。安心ですね。

  import Database.Esqueleto as E

eplist <- select $ from $ \p -> do
-- person.age IS NULL 相当
where_ $ E.isNothing (p ^. PersonAge) -- Esqueletoのプレフィックス「E.」をつける
return p

発行されるSQLは以下の通りです。ちゃんと「IS NULL」なってます。

[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`

FROM `person`
WHERE `person`.`age` IS NULL


複数条件の場合

WHERE節に複数の条件を記述する場合があります。Esqueletoでは複数の条件を「&&.」で繋げばOKです。「&&.」は見ての通り「AND」の意味になります。「OR」にしたければ「||.」になります。例では「&&.」を使います。

  let pid_list::[PersonId] = toSqlKey <$> [1, 2, 5]

eplist <- select $ from $ \p -> do
-- person.id IN (1,2,5) AND person.name = "Tom" 相当
where_ $ p ^. PersonId `in_` valList pid_list &&. p ^. PersonName ==. val "Tom"
return p

Esqueletoに限らない話ですが、演算子の優先度は関数適用よりも低いため、&&.の前後を括弧でくくる必要はありません。&&.もうっかりピリオドを忘れがちなので注意しましょう。

発行されるSQLは下記の通りです。シンプルにANDが使われています。

[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`

FROM `person`
WHERE (`person`.`id` IN (?, ?, ?)) AND (`person`.`name` = ?)
; [PersistInt64 1,PersistInt64 2,PersistInt64 5,PersistText "Tom"]


NOTイコール

Haskellでは「==」の反対は「/=」です。EsqueletoではSQLに合わせて「!=.」となっています。それだけです(例は不要でしょう)


まとめ

最初のうちは、コンパイルエラーが出ても「何が悪いのか」がよくわからないことが多いのですが、慣れてくると不思議なもので、エラーが出ても、ものの数秒でミスがわかるようになります。やはり慣れです。本記事の範囲だけだと、ごく簡単なSQLしか扱えないのですが、それでも型安全のため、例えば「PersonIdを使って(間違って)違うテーブルをselectする」というようなミスは起きなくなります(そのようなコードを書いたらコンパイルエラーになるため)。

次回はORDER BY、LIMIT、GROUP BY等を中心に扱います。