1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-09-28

この章ではDB接続ライブラリであるZIO Quillを例にZIO系ライブラリの使い方について見ていきます。

ZIOエコシステム

ZIOに付随する、ZIOと併用することが前提となっているZIO系のライブラリについては公式のZIO Ecosystemにまとめられています。
通常のScala,JavaライブラリについてもZIOを通して問題なく使えるのですが、これらのライブラリを使うことによるメリットについては、今現在の私の経験から、以下の点が挙げられると考えています。

  1. ZIOに元々備わる並列処理を特別な設定の必要なく利用できる点
    • 次章の内容とも関連しますが、ZIO用のライブラリであればZIOの実行システムの中で動作するため、ZIOの並列処理やエラー処理などの恩恵を受けやすくなると考えられます。通常のライブラリでもZIOに即して利用することでこれらの恩恵を受けることは出来るのですが、その場合自分自身でそれらを実現できるように処理を作る必要があり、バグなども起きやすくなると考えています。
  2. 統一的な、整然としたコードとなる点
    • ZIO用のライブラリについてはZIOと統一的な記法で書き、利用することが一般的です。そのため、通常のライブラリを使う場合よりもよりコード自体がよりZIOそのものと統一的でわかりやすくなると考えています。

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

MySQLコンテナの準備

ライブラリから接続するためのローカル上のMySQLをDockerを使い、まず準備します。
追加するファイルは以下の3点です。

  1. MySQL用Dockerfile
  2. ZIO用Dockerfile
  3. 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ユーザを作成しています。
以下の内容になっています。

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コンテナのみを起動し、接続し、確認することも出来ます。

# 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イメージ上で実行するという内容になっています。

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.sbtbuild.sbtに対して以下を追記します。

plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
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-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にライブラリ利用のための設定を追加するのみとなります。

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を作成します。

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を使っているため、maximumPoolSizeconnectionTimeoutといった設定はHikariCPの設定になります。
HikariCPの設定項目については以下をご覧ください。

この設定内容を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. まずdsLayerapplication.confの設定を読み込み、コネクションを確立しています。特に指定を行わなくともfromPrefix("mysql")とすること自動的にapplication.confの該当箇所から設定を読み込みます。

  2. 次にquillLayerでMySQLからカラム名等を読み込む際の設定をSnakeCaseとしています。これはテーブルのカラム名の命名規則がactor_idというようにスネークケースとなっていることをライブラリに対して伝えています。

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

データ取得処理

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

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

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

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がこれに対応します。
他の列に関しても同様です。

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)
}
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)))
}

上記について特に重要な以下の部分について説明します。

  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の設定を追加します。

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)
    }
}

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
  )

これで必要設定、処理の追加が全て終わりました。
以下のコマンドでプログラムを実行しましょう。

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並列処理について見ていきます。

前章:ZIOのエラー処理
次章:ZIOの並列処理

演習

  1. 今回見てきたものについて実際に動かし、挙動を確かめてください。
  2. 上記のQDSL以外のQDSLを試してください。QDSLの使い方についてはこのページで調べられます。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?