ZIOライブラリ利用(DB接続)

この章ではDB接続ライブラリである[ZIO Quill](https://zio.dev/zio-quill/)を例にZIO系ライブラリの使い方について見ていきます。
# ZIOエコシステム
ZIOに付随する、ZIOと併用することが前提となっているZIO系のライブラリについては公式の[ZIO Ecosystem](https://zio.dev/ecosystem/)にまとめられています。
通常のScala,JavaライブラリについてもZIOを通して問題なく使えるのですが、これらのライブラリを使うことによるメリットについては、今現在の私の経験から、以下の点が挙げられると考えています。
1. ZIOに元々備わる並列処理を特別な設定の必要なく利用できる点
    - 次章の内容とも関連しますが、ZIO用のライブラリであればZIOの実行システムの中で動作するため、ZIOの並列処理やエラー処理などの恩恵を受けやすくなると考えられます。通常のライブラリでもZIOに即して利用することでこれらの恩恵を受けることは出来るのですが、その場合自分自身でそれらを実現できるように処理を作る必要があり、バグなども起きやすくなると考えています。
1. 統一的な、整然としたコードとなる点
    - ZIO用のライブラリについてはZIOと統一的な記法で書き、利用することが一般的です。そのため、通常のライブラリを使う場合よりもよりコード自体がよりZIOそのものと統一的でわかりやすくなると考えています。

# ライブラリ利用のためのセットアップ

## MySQLコンテナの準備
ライブラリから接続するためのローカル上のMySQLをDockerを使い、まず準備します。
追加するファイルは以下の3点です。
1. MySQL用Dockerfile
1. ZIO用Dockerfile
1. docker-compose.yml

これらは以下の様に配置します。

```shell
.
└── zio-hands-on
    ├── mysql-test
    │   └── Dockerfile
    ├── project
    │   └── plugins.sbt
    ├── src
    ├── Dockerfile
    ├── docker-compose.yml
    └── build.sbt
```
MySQLコンテナのイメージの概要としてはMySQLのイメージを元に、[Sakila Sample Database](https://dev.mysql.com/doc/sakila/en/)のDBをこのMySQL内に構築し、このDBへアクセスするためのsakilaユーザを作成しています。
以下の内容になっています。
```Dockerfile:mysql-test/Dockerfile
FROM mysql:8-debian

ENV MYSQL_ROOT_PASSWORD=root

RUN apt update \
    && apt install -y wget unzip \
    && wget http://downloads.mysql.com/docs/sakila-db.zip \
    && unzip sakila-db.zip \
    && mv sakila-db/sakila-schema.sql /docker-entrypoint-initdb.d/01_sakila-schema.sql \
    && mv sakila-db/sakila-data.sql /docker-entrypoint-initdb.d/02_sakila-data.sql \
    && echo "CREATE USER sakila@'%' IDENTIFIED BY 'sakila';" > /docker-entrypoint-initdb.d/03_create_test_user.sql  \
    && echo "GRANT ALL PRIVILEGES ON *.* TO sakila@'%';" >> /docker-entrypoint-initdb.d/03_create_test_user.sql  \
    && echo "FLUSH PRIVILEGES;" >> /docker-entrypoint-initdb.d/03_create_test_user.sql
```

``Sakila Sample Database``についてまず確認されたいといった場合は以下の様にMySQLコンテナのみを起動し、接続し、確認することも出来ます。
```shell
# MySQLのDockerfileがあるディレクトリまで移動します。
cd mysql-test
docker build -t sakila-sample . 
docker run --name sakila-sample -d sakila-sample -p 3306:3306
# rootユーザでログインします。
docker container exec -it sakila-sample mysql -uroot -proot -Dsakila
# 以下種々のSQLを発行することで確認できます。
```

ZIOのイメージについてはまずsbtのイメージを利用して実行用のfat jarを作成し、それをjavaイメージ上で実行するという内容になっています。
```Dockerfile:mysql-test/Dockerfile
# first stage

FROM sbtscala/scala-sbt:eclipse-temurin-17.0.3_1.7.1_2.13.8 AS build

COPY ./ ./app

WORKDIR app

RUN sbt assembly

# second stage

FROM eclipse-temurin:17.0.4_8-jre

COPY --from=build /root/app/target/scala-3.3.0/*.jar /zio-hands-on.jar
```

far jar作成用のプラグインを利用するための設定とfat jarのパッケージングの際の設定のために``plugins.sbt``と``build.sbt``に対して以下を追記します。
```scala:plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
```
```scala:build.sbt
ThisBuild / assemblyMergeStrategy := {
  case PathList(ps @ _*) if ps.last == "module-info.class" =>
    MergeStrategy.discard
  case PathList("io", "getquill", xs @ _*) => MergeStrategy.first
  case x =>
    val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
    oldStrategy(x)
}
```
最後にこれらZIOのコンテナとMySQLのコンテナを動かす``docker-compose.yml``を作成します。
こちらについてはMySQLコンテナに対してsakilaユーザでの接続が可能となった段階でZIOコンテナを起動させるという設定内容となっています。

```docker:docker-compose.yml
version: '3.9'
services:
  zio:
    build: .
    container_name: "zio"
    depends_on:
      sakilamysql:
        condition: service_healthy
    command: java -jar zio-hands-on.jar
  sakilamysql:
    build: ./mysql-test
    container_name: "sakilamysql"
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -u sakila --password=sakila
      start_period: 5s
      interval: 5s
      timeout: 5s
      retries: 100
```

## ZIO Quill利用のための設定
``ZIO Quill``を使うための設定については``build.sbt``にライブラリ利用のための設定を追加するのみとなります。
```scala:build.sbt
    // DB関係
    libraryDependencies ++= Seq(
      "mysql"        % "mysql-connector-java" % "8.0.17",
      "io.getquill" %% "quill-jdbc-zio"       % "4.6.0.1"
    )
```
# DBからのデータ取得
## MySQLへの接続設定
それでは必要な設定が完了したので、DBからのデータ取得を行っていきたいと思います。
まずは先ほど作成したMySQLへのsakilaユーザを使った接続設定を行っていきます。

``resources``ディレクトリを作成し、以下の内容で``application.conf``を作成します。
```conf:application.conf
# テスト用MySQL接続のための設定
mysql {
  dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
  dataSource.url="jdbc:mysql://sakilamysql:3306/sakila"
  dataSource.user=sakila
  dataSource.password=sakila
  dataSource.cachePrepStmts=true
  dataSource.prepStmtCacheSize=250
  dataSource.prepStmtCacheSqlLimit=2048
  connectionTimeout=30000
  maximumPoolSize=1
}
```
``ZIO Quill``はコネクションプールとしてHikariCPを使っているため、``maximumPoolSize``や``connectionTimeout``といった設定はHikariCPの設定になります。
HikariCPの設定項目については以下をご覧ください。

https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby

この設定内容をScala側で読み込むために以下のファイルを作成します。

```scala:MySQLContext.scala
case class MySQLContext(q: Quill.Mysql[SnakeCase.type])
object MySQLContext {

  val dsLayer: ZLayer[Any, Throwable, DataSource] =
    Quill.DataSource.fromPrefix("mysql")

  val quillLayer: ZLayer[DataSource, Nothing, Quill.Mysql[SnakeCase.type]] =
    Quill.Mysql.fromNamingStrategy(SnakeCase)

  val mysqlLayer: ZLayer[Any, Throwable, MySQLContext] =
    (this.dsLayer >>> this.quillLayer)
      .map(zEnv => ZEnvironment(MySQLContext(zEnv.get)))

}
```
少し内容が複雑になっているのですが、順に説明します。

1. まず``dsLayer``で``application.conf``の設定を読み込み、コネクションを確立しています。特に指定を行わなくとも``fromPrefix("mysql")``とすること自動的に``application.conf``の該当箇所から設定を読み込みます。

1. 次に``quillLayer``でMySQLからカラム名等を読み込む際の設定を``SnakeCase``としています。これはテーブルのカラム名の命名規則が``actor_id``というようにスネークケースとなっていることをライブラリに対して伝えています。
1. 最後に``dsLayer``を``quillLayer``に対してDIし、map関数により最終的に``MySQLContext``でラップします。
``dsLayer``,``quillLayer``の型をそれぞれ見ると``ZLayer[Any, Throwable, DataSource]``,``ZLayer[DataSource, Nothing, Quill.Mysql[SnakeCase.type]]``となっており、``dsLayer``は何にも依存していない一方で、``quillLayer``は``dsLayer``に依存しているため、このようにDIします。以前登場した``provide``関数が自動で型を解決しDIを行う関数になりますが、``>>>``は自分自身で手動でDIを行うための関数になります。
``MySQLContext``で全体をラップすることの意味についてですが、これはZIOのDIが型により行われることに起因しています。例えば複数のMySQLに同時に接続するアプリケーションを考えた場合、このような``case class``でラップしないとDIのタイミングで``Quill.Mysql[SnakeCase.type]``型のものが複数存在することになり、型を基準にするDIが``provide``関数などでは上手くいかないため、予めこのようにラップを行っています。

## データ取得処理
必要な設定がすべて終わりましたので、いよいよデータ取得処理の作成を行っていきます。

今回はsakilaのactorテーブルのデータを読み取る処理を作りたいと思います。
sakilaのDBのテーブルについては以下を確認してください。

https://dev.mysql.com/doc/sakila/en/sakila-structure.html

まず、データを格納する``case class``と、サービスパターンを使い、データ取得処理を実際に行う関数を作ります。

```scala:Actor.scala
import java.util.Date
final case class Actor (actorId: Int,
                        firstName: String,
                        lastName: String,
                        lastUpdate: Date)
```
実際のactorテーブルの列としては例えば``actor_id``という列がありますが、先ほどの``Quill.Mysql.fromNamingStrategy(SnakeCase)``の設定により``actorId``がこれに対応します。
他の列に関しても同様です。

```scala:ActorRepo.scala
import domain.model.Actor
import zio._
import java.sql.SQLException

trait ActorRepo {
  def getActor: ZIO[Any, SQLException, List[Actor]]
}

object ActorRepo {
  def getActor: ZIO[ActorRepo, SQLException, List[Actor]] =
    ZIO.serviceWithZIO[ActorRepo](_.getActor)
}
```
```scala:ActorRepoImpl.scala
import domain.model.Actor
import domain.repository.ActorRepo
import infrastructure.config.MySQLContext

import io.getquill.*
import zio.*

import java.sql.SQLException

object ActorRepoImpl {
  val layer: ZLayer[MySQLContext, Nothing, ActorRepoImpl] =
    ZLayer.fromFunction(ActorRepoImpl.apply _)
}

final case class ActorRepoImpl(ctx: MySQLContext) extends ActorRepo {
  import ctx.q.*
  def getActor: ZIO[Any, SQLException, List[Actor]] =
    run(quote(query[Actor].sortBy(a => a.actorId)))
}
```

上記について特に重要な以下の部分について説明します。
```scala
  import ctx.q.*
  def getActor: ZIO[Any, SQLException, List[Actor]] =
    run(quote(query[Actor].sortBy(a => a.actorId)))
```
まず、これは``Quoted Domain Specific Language (QDSL)``というScalaの記法に則ってクエリを作成できるDSLによりactorテーブルから``actor_id``列に対して昇順に並び替えた上でデータを取得しています。
``import ctx.q.*``に関してはQuillを使う上で必須のimportとなりますので、必ず指定します。

最後に上記の関数を利用してコンソールに取得したデータを出力する処理とDIの設定を追加します。

```scala:ApplicationServiceImpl.scala
import jp.webcrew.hands.on.zio.application.service.ApplicationService
import jp.webcrew.hands.on.zio.domain.repository.ActorRepo
import zio.{Console, ZIO, ZLayer}

import java.text.SimpleDateFormat
import java.util.Date

case class ApplicationServiceImpl(currentDate: Date, actorRepo: ActorRepo) extends ApplicationService {
  override def consoleOutput(): ZIO[Any, Throwable, Unit] =
    for {
      _ <- Console.printLine(
        s"${new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(currentDate)} Hello, World!"
      )
      actors <- actorRepo.getActor
      _      <- Console.printLine("The head actor:" + actors.head.toString)
    } yield ()
}

object ApplicationServiceImpl {
  val layer: ZLayer[Date & ActorRepo, Nothing, ApplicationService] =
    ZLayer {
      for {
        currentDate <- ZIO.service[Date]
        actorRepo   <- ZIO.service[ActorRepo]
      } yield ApplicationServiceImpl(currentDate, actorRepo)
    }
}

```
```scala:MainApp.scala
  def run: ZIO[Any, Throwable, Unit] = ApplicationService.consoleOutput().provide(
    ZLayer.fromZIO(ZIO.attempt {
      import java.text.SimpleDateFormat
      // 任意の日付文字列
      val inpDateStr = "2023/07/25 17:46:00"

      // 取り扱う日付の形にフォーマット設定
      val sdformat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")

      // Date型に変換( DateFromatクラスのparse() )
      sdformat.parse(inpDateStr)
    }),
    ApplicationServiceImpl.layer,
    ActorRepoImpl.layer,
    mysqlLayer
  )
```

これで必要設定、処理の追加が全て終わりました。
以下のコマンドでプログラムを実行しましょう。
```shell
docker compose up --abort-on-container-exit --build
```
以下のように出力されたら成功です。
```shell
(前略)
sakilamysql  | 2023-08-02T07:35:28.670266Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
sakilamysql  | 2023-08-02T07:35:28.694505Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
sakilamysql  | 2023-08-02T07:35:28.993075Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
sakilamysql  | 2023-08-02T07:35:28.993192Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.34'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
zio          | SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
zio          | SLF4J: Defaulting to no-operation (NOP) logger implementation
zio          | SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
zio          | 2023/07/25 17:46:00 Hello, World!
zio          | The head actor:Actor(1,PENELOPE,GUINESS,Wed Feb 15 04:34:33 UTC 2006)
zio exited with code 0
(後略)
```

# 終わりに
今回は``ZIO Quill``を通してZIOエコシステムのライブラリの使い方を見てきました。
比較的設定が複雑なライブラリだったかと思いますので、他のライブラリについては比較的簡単に使用できるようになったかと思います。
次章では一転してZIO並列処理について見ていきます。

前章:[ZIOのエラー処理](https://qiita.com/wc_sangen/private/44b57489319b01765a06)
次章:[ZIOの並列処理](https://qiita.com/wc_sangen/private/5154da992e0fb67166fb)

# 演習

1. 今回見てきたものについて実際に動かし、挙動を確かめてください。
    - 解答例は以下になります。
    https://github.com/hatuda/zio-practice/tree/CHAPTER4
1. 上記のQDSL以外のQDSLを試してください。QDSLの使い方については[このページ](https://zio.dev/zio-quill/writing-queries)で調べられます。
    - 解答例は以下になります。
    https://github.com/hatuda/zio-practice/tree/CHAPTER4-Plus