Help us understand the problem. What is going on with this article?

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等を中心に扱います。

cyclone_t
IoTコンサル勤務。Haskellやってます。
https://twitter.com/cyclone_tr
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした