Play + ZIO Quillハンズオン

## 目的
Scala + PlayFramework + ZIO Quillで簡単なREST APIを作成するハンズオンです。
DBはPlayFramework内蔵のH2インメモリーデータベースを使用します。

このハンズオンではAPIのフレームワークとしてPlayFrameworkを使用していますが、PlayFrameworkの記法については触れず、Quillの記法メインで解説します。

### ZIO Quillとは
Scalaのライブラリである[ZIO](https://zio.dev/)のDBアクセスライブラリです。（ORMみたいなもの）
ZIO本体が無くても使用できます。

https://zio.dev/zio-quill/

#### 特長
1. テーブルとのマッピングはケースクラスを用意するだけ
2. 生のSQLを書かずに用意されているメソッドだけでCRUD処理が書ける（値の格納はバインド変数で行う）
3. コンパイル時に[2.]のメソッドを解析し、生のクエリを生成しコンパイルログに出力できる

### この記事を作成した経緯
ScalaのDBアクセスライブラリの一番人気はslickですが、Scala3の対応が遅れていました。
代替案としてScala3対応が早かったQuillで開発を進めたところ難なく使えることが分かり、Quillに気軽に触れてもらいたいと思いハンズオン記事を作成しました。


## 構成
使用するフレームワークおよびバージョンは以下の通りです。

- Scala: 3.3.1
- PlayFramework: 2.9.x
- zio-quill: 4.8.0

## 前提条件
- JDK 1.7以降がインストールされていること
- ビルドツール(sbt)がインストールされていること

※ 筆者はwindows環境で実装しているため、ターミナルはコマンドプロンプトを使用しています。

## ゴール
ユーザ情報のCRUDを行う簡単なREST APIを作成します。

## プロジェクト作成

### 新規プロジェクト作成

コマンドプロンプトでプロジェクトを作成したいディレクトリに移動し、以下のコマンドを実行します。
※sbtには`sbt new`というコマンドがあり、これを使うとPlayFrameworkのひな型を作ってくれます。
```
> sbt new playframework/play-scala-seed.g8 --branch 2.9.x
```

プロジェクト名やバージョンを聞かれます。下記のように入力してください。
- name: play-quill-hands-on
- scala_version: 3.3.1

```
This template generates a Play Scala project

name [play-scala-seed]: play-quill-hands-on
organization [com.example]:
play_version [2.9.2]:
scala_version [2.13.13]: 3.3.1
sbt_giter8_scaffold_version [0.16.2]:
```

`play-quill-hands-on`ディレクトリの`build.sbt`にQuill、H2を使用するための設定を追記します。
``` build.sbt
// ↓↓↓↓ここから追加↓↓↓↓
libraryDependencies ++= Seq(
  "com.h2database" % "h2"         % "1.4.192",
  "io.getquill"   %% "quill-jdbc" % "4.8.0" exclude ("org.scala-lang.modules", "scala-java8-compat_3"),
  evolutions,
  jdbc
)
// ↑↑↑↑ここまで追加↑↑↑↑
```


### DBの準備
今回はH2を使用するため、`conf/application.conf`にデフォルトDBとしてH2を指定します。
``` application.conf
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play;MODE=MYSQL"
db.default.username=sa
db.default.password=""
``` 

PlayFrameworkの`Evolutions`を使用してユーザ情報テーブルの作成と初期データの登録を行うため、下記のファイルを配置します。

``` conf/evolutions/default/1.sql
# Users schema

# --- !Ups

CREATE TABLE User (
                      id bigint(20) NOT NULL AUTO_INCREMENT,
                      email varchar(255) NOT NULL,
                      password varchar(255) NOT NULL,
                      name varchar(255) NOT NULL,
                      PRIMARY KEY (id)
);
INSERT INTO User VALUES (1, 'test@test.test', 'pass', 'テスト テスト');
INSERT INTO User VALUES (2, 'test2@test.test', 'aaaa', 'テスト2 テスト2');

# --- !Downs

DROP TABLE User;
```

※Evolutionsの仕様については[Playのドキュメント](https://www.playframework.com/documentation/2.9.x/Evolutions)を参照してください。



### 起動確認
作成した`play-quill-hands-on`ディレクトリに移動し、以下のコマンドでプロジェクトを実行します。
``` 
> sbt run
``` 

ブラウザから`http://localhost:9000`にアクセスするとEvolutionsの確認画面が表示されます。
※Evolutionsにより[DBの準備]で作成したsqlファイルのCREATEとINSERTが実行されるため、起動と同時にDBの構築も完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620288/e44bf0cb-a9c8-19c1-717b-a4e0754310ca.png)

上記画面の`Click here to apply this script now!`ボタンをクリックし、画面に「Welcome to Play!」と表示されれば起動は完了です。


## Quillの設定
### DB接続設定
QuillでDBアクセスするため、`play-quill-hands-on`プロジェクトの`conf/application.conf`に以下の設定を追加します。
``` application.conf
h2.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
h2.dataSource.url="jdbc:h2:mem:play;MODE=MYSQL"
h2.dataSource.user=sa
```
※H2以外のDBの設定については[Quillのドキュメント](https://zio.dev/zio-quill/contexts)を参照してください。


### ケースクラスの作成
Quillではテーブルとのマッピングを単純なケースクラスで行います。
下記のように、`models`パッケージにUserテーブルのカラムを持つケースクラスを作成してください。
```app/models/User.scala
package models

case class User(id: Long, email: String, password: String, name: String)
```

## ルーティングの定義
### コントローラの雛形を作る
`controllers`パッケージに`UserController`クラスを以下のように作成します。

```app/controllers/UserController.scala
package controllers

import io.getquill.*
import models.User
import play.api.*
import play.api.data.Form
import play.api.data.Forms.*
import play.api.libs.json.*
import play.api.mvc.*
import javax.inject.*

@Singleton
class UserController @Inject() (val controllerComponents: ControllerComponents) extends BaseController {

  val ctx = new H2JdbcContext(SnakeCase, "h2")
  import ctx.*

  def list = TODO

  def get(id: Long) = TODO

  def create = TODO

  def update(id: Long) = TODO

  def delete(id: Long) = TODO



  // リクエスト（request -> UserForm）
  object UserForm {
    case class UserForm(email: String, password: String, name: String)

    object UserForm {
      def unapply(req: UserForm): Option[(String, String, String)] = {
        Some(req.email, req.password, req.name)
      }
    }

    val userForm = Form(
      mapping(
        "email"    -> nonEmptyText,
        "password" -> nonEmptyText,
        "name"     -> nonEmptyText
      )(UserForm.apply)(UserForm.unapply)
    )
  }

  // レスポンス（User -> json）
  implicit val userFormat: Format[User] = Json.format
}
```

#### 解説

```scala
val ctx = new H2JdbcContext(SnakeCase, "h2")
```
> QuillでDBアクセスを行うためのコンテキストを宣言します。
`H2JdbcContext`はH2用のコンテキストです。第2引数の`"h2"`が`application.conf`で定義した`h2.xxxx`に対応しています。

```scala
import ctx.*
```
> コンテキストインスタンス内のすべてのメソッドを使うための宣言です。


### ルーティングの設定
APIのURLを`conf/routes`に定義します。下記を追記してください。
```
# 参照（全件）
GET           /users               controllers.UserController.list
# 参照（1件）
GET           /users/:id           controllers.UserController.get(id: Long)
# 登録
POST          /users               controllers.UserController.create
# 更新
PUT           /users/:id           controllers.UserController.update(id: Long)
# 削除
DELETE        /users/:id           controllers.UserController.delete(id: Long)
```

## 参照（全件）APIの実装
### コントローラ
`UserController`の`list`メソッドを以下のように実装します。
```scala
  def list = Action { implicit request: Request[AnyContent] =>
    // Userテーブルをselect（全件）
    inline def q = quote { query[User] }
    val list     = ctx.run(q)

    // レスポンス（json）
    Ok(Json.toJson(list))
  }
```
#### 解説
```scala
    // Userテーブルをselect（全件）
    inline def q = quote { query[User] }
```
> `quote`の中身に実行するクエリを定義します。
`query[User]`でUserテーブルに対して操作することを示しています。
条件なしのselectの場合、これだけでUserテーブルのselect文になります。

```scala
    val list     = ctx.run(q)
```
 > `run`で`quote`の中身を実行します。
 selectの場合、`run`の戻り値はケースクラスのリストになります。

この状態で`sbt compile`でコンパイルすると、下記のようなコンパイルログが出力され、生成される生のSQLを確認することができます。
```
[info] 29 |    val list = ctx.run(q)
[info]    |               ^^^^^^^^^^
[info]    |Quill Query (compiled in 199ms): SELECT x.id, x.email, x.password, x.name FROM user x
```
※補足：`inline`について
`quote`の宣言に`inline`を使用していますが、`val q    = quote { query[User] }`のようにvalで宣言することも可能です。
ただし、valの場合はコンパイルログに上記のように生成されるSQLが出力されません。
詳細については[zio-protoquill](https://github.com/zio/zio-protoquill?tab=readme-ov-file#introduction)を確認してください。



### 実行
コマンドプロンプトから`curl`コマンドなどで下記のURLを実行します。
`GET	http://localhost:9000/users`
（`sbt run`を停止していた場合は[プロジェクト作成 > 起動確認]を再度実行してください）
Userテーブルのデータがjsonで全件出力されることを確認してください。
```
> curl http://localhost:9000/users
[{"id":1,"email":"test@test.test","password":"pass","name":"テスト テスト"},{"id":2,"email":"test2@test.test","password":"aaaa","name":"テスト2 テスト2"}]
```

## 参照（1件）APIの実装
### コントローラ
`UserController`の`get`メソッドを以下のように実装します。
```scala
  def get(id: Long) = Action { implicit request: Request[AnyContent] =>
    // Userテーブルをselect（1件）
    inline def q = quote {
      query[User].filter(_.id == lift(id))
    }
    val list = ctx.run(q)

    Ok(Json.toJson(list))
  }
```
#### 解説
```scala
    // Userテーブルをselect（1件）
    inline def q = quote {
      query[User].filter(_.id == lift(id))
    }
```
> `filter(_.id == lift(id))`でselect文のwhere句を定義しています。
`lift`により引き数のidをバインドしています。
※`filter`の詳細：	https://zio.dev/zio-quill/writing-queries#filter		
※`lift`について：	https://zio.dev/zio-quill/writing-queries#lifted-values		

コンパイルログは下記のようになります。
```
[info] 35 |    val list = ctx.run(q)
[info]    |               ^^^^^^^^^^
[info]    |Quill Query (compiled in 35ms): SELECT x1.id, x1.email, x1.password, x1.name FROM user x1 WHERE x1.id = $1
```

### 実行
下記のURLを実行します。
`GET	http://localhost:9000/users/1`

Userテーブルのid=1のデータのみ出力されることを確認してください。
```
> curl http://localhost:9000/users/1
[{"id":1,"email":"test@test.test","password":"pass","name":"テスト テスト"}]
```

## 登録APIの実装
### コントローラ
`UserController`の`create`メソッドを以下のように実装します。
```scala
  def create = Action { implicit request: Request[AnyContent] =>
    import UserForm.*
    def failure(badForm: Form[UserForm]) =
      BadRequest(Json.parse(s"""{"error": "${badForm.errors.toString()}"}"""))

    def success(form: UserForm) =
      // Userテーブルをinsert
      inline def q = quote {
        query[User].insert(
          _.email    -> lift(form.email),
          _.password -> lift(form.password),
          _.name     -> lift(form.name)
        )
      }
      val count = ctx.run(q)
      Ok(Json.parse(s"""{"count": ${count}}"""))

    userForm.bindFromRequest().fold(failure, success)
  }
```
#### 解説
```scala
      // Userテーブルをinsert
      inline def q = quote {
        query[User].insert(
          _.email    -> lift(form.email),
          _.password -> lift(form.password),
          _.name     -> lift(form.name)
        )
      }
      val count = ctx.run(q)
```
> `insert`メソッドでUserテーブルのinsert文を定義します。
`run`の戻り値は登録件数になります。
※`insert`の詳細：	https://zio.dev/zio-quill/writing-queries#insertvalue--insert

コンパイルログは下記のようになります。
```
[info] 54 |      val count = ctx.run(q)
[info]    |                  ^^^^^^^^^^
[info]    |Quill Query (compiled in 21ms): INSERT INTO user (email,password,name) VALUES ($1, $2, $3)
```

### 実行
リクエストボディに登録情報（json）を指定し、下記のURLを実行します。
`POST	http://localhost:9000/users`

登録件数が返ってくることを確認してください。
```
> curl http://localhost:9000/users -X POST -H  "Content-Type: application/json" -d "{\"email\":\"bbb@bbb.bbb\",\"password\":\"bbbbbbb\",\"name\":\"山田 太郎\"}"
{"count":1}
```

#### 実行結果確認
登録結果を確認するため、参照（全件）APIを実行し、登録したデータが追加されていることを確認してください。
```
> curl http://localhost:9000/users
...
,{"id":3,"email":"bbb@bbb.bbb","password":"bbbbbbb","name":"山田 太郎"}]
```

## 更新APIの実装
### コントローラ
`UserController`の`update`メソッドを以下のように実装します。
```scala
  def update(id: Long) = Action { implicit request: Request[AnyContent] =>
    import UserForm.*
    def failure(badForm: Form[UserForm]) =
      BadRequest(Json.parse(s"""{"error": "${badForm.errors.toString()}"}"""))

    def success(form: UserForm) =
      // Userテーブルをupdate
      inline def q = quote {
        query[User]
          .filter(_.id == lift(id))
          .update(
            _.email    -> lift(form.email),
            _.password -> lift(form.password),
            _.name     -> lift(form.name)
          )
      }
      val count = ctx.run(q)
      Ok(Json.parse(s"""{"count": ${count}}"""))

    userForm.bindFromRequest().fold(failure, success)
  }
```
#### 解説
```scala
      // Userテーブルをupdate
      inline def q = quote {
        query[User]
          .filter(_.id == lift(id))
          .update(
            _.email    -> lift(form.email),
            _.password -> lift(form.password),
            _.name     -> lift(form.name)
          )
      }
      val count = ctx.run(q)
```
> `filter`メソッドでupdate文のwhere句を定義します。
`update`メソッドでupdate文のset句を定義します。
`run`の戻り値は更新件数になります。
※`update`の詳細：	https://zio.dev/zio-quill/writing-queries#updatevalue--update

コンパイルログは下記のようになります。
```
[info] 76 |      val count = ctx.run(q)
[info]    |                  ^^^^^^^^^^
[info]    |Quill Query (compiled in 14ms): UPDATE user AS x5 SET email = $1, password = $2, name = $3 WHERE x5.id = $4
```

### 実行
リクエストボディに更新情報（json）を指定し、下記のURLを実行します。
`PUT	http://localhost:9000/users/1`

更新件数が返ってくることを確認してください。
```
> curl http://localhost:9000/users/1 -X PUT -H  "Content-Type: application/json" -d "{\"email\":\"mod@aaa.aaa\",\"password\":\"mod\",\"name\":\"テスト 変更\"}"
{"count":1}
```

#### 実行結果確認
更新結果を確認するため参照（1件）APIを実行し、id=1のデータが更新されていることを確認してください。
```
> curl http://localhost:9000/users/1
[{"id":1,"email":"mod@aaa.aaa","password":"mod","name":"テスト 変更"}]
```

## 削除APIの実装
### コントローラ
`UserController`の`delete`メソッドを以下のように実装します。
```scala
  def delete(id: Long) = Action { implicit request: Request[AnyContent] =>
    // Userテーブルをdelete
    inline def q = quote {
      query[User].filter(_.id == lift(id)).delete
    }
    val count = ctx.run(q)
    Ok(Json.parse(s"""{"count": ${count}}"""))
  }
```
#### 解説
```scala
    // Userテーブルをdelete
    inline def q = quote {
      query[User].filter(_.id == lift(id)).delete
    }
    val count = ctx.run(q)
```
> `filter`メソッドでdelete文のwhere句を定義します。
`run`の戻り値は削除件数になります。

コンパイルログは下記のようになります。
```
[info] 89 |    val count = ctx.run(q)
[info]    |                ^^^^^^^^^^
[info]    |  Quill Query (compiled in 4ms): DELETE FROM user AS x9 WHERE x9.id = $1
```

### 実行
下記のURLを実行します。
`DELETE	http://localhost:9000/users/1`

削除件数が返ってくることを確認してください。
```
> curl http://localhost:9000/users/1 -X DELETE
{"count":1}
```

#### 実行結果確認
削除結果を確認するため参照（1件）APIを実行し、下記のようにid=1のデータが存在しないことを確認してください。
```
> curl http://localhost:9000/users/1
[]
```

## おわりに
ZIO Quillを使って簡単にDBのCRUD処理が実装できました。

上記の基本的なクエリ以外にも、`GROUP BY`・集約関数・テーブル結合なども簡単に実装出来ます。
また、Quillにはコネクションプールやトランザクションの仕組みも用意されています。
詳しくは、Quillのドキュメントを確認してみてください。

https://zio.dev/zio-quill/writing-queries

https://zio.dev/zio-quill/contexts/#connection-pool-configuration