この章ではDB接続ライブラリであるZIO Quillを例にZIO系ライブラリの使い方について見ていきます。
ZIOエコシステム
ZIOに付随する、ZIOと併用することが前提となっているZIO系のライブラリについては公式のZIO Ecosystemにまとめられています。
通常のScala,JavaライブラリについてもZIOを通して問題なく使えるのですが、これらのライブラリを使うことによるメリットについては、今現在の私の経験から、以下の点が挙げられると考えています。
- ZIOに元々備わる並列処理を特別な設定の必要なく利用できる点
- 次章の内容とも関連しますが、ZIO用のライブラリであればZIOの実行システムの中で動作するため、ZIOの並列処理やエラー処理などの恩恵を受けやすくなると考えられます。通常のライブラリでもZIOに即して利用することでこれらの恩恵を受けることは出来るのですが、その場合自分自身でそれらを実現できるように処理を作る必要があり、バグなども起きやすくなると考えています。
- 統一的な、整然としたコードとなる点
- ZIO用のライブラリについてはZIOと統一的な記法で書き、利用することが一般的です。そのため、通常のライブラリを使う場合よりもよりコード自体がよりZIOそのものと統一的でわかりやすくなると考えています。
ライブラリ利用のためのセットアップ
MySQLコンテナの準備
ライブラリから接続するためのローカル上のMySQLをDockerを使い、まず準備します。
追加するファイルは以下の3点です。
- MySQL用Dockerfile
- ZIO用Dockerfile
- docker-compose.yml
これらは以下の様に配置します。
.
└── zio-hands-on
├── mysql-test
│ └── Dockerfile
├── project
│ └── plugins.sbt
├── src
├── Dockerfile
├── docker-compose.yml
└── build.sbt
MySQLコンテナのイメージの概要としてはMySQLのイメージを元に、Sakila Sample DatabaseのDBをこのMySQL内に構築し、このDBへアクセスするためのsakilaユーザを作成しています。
以下の内容になっています。
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コンテナのみを起動し、接続し、確認することも出来ます。
# 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イメージ上で実行するという内容になっています。
# 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
に対して以下を追記します。
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
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コンテナを起動させるという設定内容となっています。
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
にライブラリ利用のための設定を追加するのみとなります。
// 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
を作成します。
# テスト用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の設定項目については以下をご覧ください。
この設定内容を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)))
}
少し内容が複雑になっているのですが、順に説明します。
-
まず
dsLayer
でapplication.conf
の設定を読み込み、コネクションを確立しています。特に指定を行わなくともfromPrefix("mysql")
とすること自動的にapplication.conf
の該当箇所から設定を読み込みます。 -
次に
quillLayer
でMySQLからカラム名等を読み込む際の設定をSnakeCase
としています。これはテーブルのカラム名の命名規則がactor_id
というようにスネークケースとなっていることをライブラリに対して伝えています。 -
最後に
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のテーブルについては以下を確認してください。
まず、データを格納するcase class
と、サービスパターンを使い、データ取得処理を実際に行う関数を作ります。
import java.util.Date
final case class Actor (actorId: Int,
firstName: String,
lastName: String,
lastUpdate: Date)
実際のactorテーブルの列としては例えばactor_id
という列がありますが、先ほどのQuill.Mysql.fromNamingStrategy(SnakeCase)
の設定によりactorId
がこれに対応します。
他の列に関しても同様です。
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)
}
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)))
}
上記について特に重要な以下の部分について説明します。
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の設定を追加します。
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)
}
}
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
)
これで必要設定、処理の追加が全て終わりました。
以下のコマンドでプログラムを実行しましょう。
docker compose up --abort-on-container-exit --build
以下のように出力されたら成功です。
(前略)
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並列処理について見ていきます。
演習
- 今回見てきたものについて実際に動かし、挙動を確かめてください。
- 解答例は以下になります。
https://github.com/hatuda/zio-practice/tree/CHAPTER4
- 解答例は以下になります。
- 上記のQDSL以外のQDSLを試してください。QDSLの使い方についてはこのページで調べられます。