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

# はじめに

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等は省略しています。

```haskell
　-- selectした結果が「eplist」に入る
  eplist <- select $ from $ \p -> do -- select, from, 関数(doブロック含む)でワンセット
    -- join, on, where_, limit, orderByなど
    return p -- returnで閉じる
```

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

```haskell
  -- これだけだとコンパイルエラーになる　
  eplist <- select $ from $ \p -> do
    return p
```

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

```haskell
-- 必要なGHC拡張
{-# LANGUAGE ScopedTypeVariables #-}

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

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

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

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

```haskell
  eplist <- select $ from $ \p -> do
    where_ $ p ^. PersonName ==. val "Tom"
    return p
```

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

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

```sql
[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の「リスト」となります。

```haskell
-- EntityからIDを取得する。pidの型はPersonId型となる
let pid = entityKey ep

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

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

```haskell
-- Entityから値を取得する。pの型はPerson型となる
let p = entityVal ep

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

## PersonIdとPersonを両方取得したい

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

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

## APIの戻り値にするには

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

```haskell
-- api_person_listは[ApiPerson]型
let api_person_list = toApiPersonFE <$> eplist
```

# WHERE節の書き方（その２）

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

## IN

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

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


```haskell
  -- デモ用に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になっていますね。

```sql
[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」をしようとすると、型があわずにコンパイルエラーとなります。安心ですね。

```haskell
  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」なってます。

```sql
[Debug#SQL] SELECT `person`.`id`, `person`.`name`, `person`.`age`, `person`.`type`
FROM `person`
WHERE `person`.`age` IS NULL
```


## 複数条件の場合

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

```haskell
  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が使われています。

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