Lambdaカクテル

京都在住Webエンジニアの日記です

ScalaアプリケーションでScalikeJDBCを使えるようにする + MySQLのセットアップ

サクサク年表君というソフトを趣味でやっている.これは年表をみんなで作れるようにするというプロジェクトで,プロジェクトといいつつ自分しかやっていないのだけれど,一人でチマチマ作っている.今回はScalikeJDBCでDBにつなげるようにしたので,備忘録的な立ち位置の記事.

バージョン

  • Java SE 8
  • sbt 1.4.5
  • Scala 2.13.4
  • ScalikeJDBC 3.5.0
  • MySQL 8.0.22

前回はこちら

blog.3qe.us

ScalikeJDBCとは

ScalikeJDBCとは,Scala向けの小さなDBアクセスライブラリだ.ORMは含まれておらず,素朴にSQLを書いて実行するとList[A]がもらえるサックリした世界観. ちなみに仕事で今いるチームでもORMはあまり使われていなくて,開発者にとって一番馴染みがあるからSQLで良いし,細やかな動作の様子を把握できるし,つねに同じようなSQLが発行されてSlow Queryを探しやすくなるので良い,という理由で直にSQLを書いている.自動クエリ組み立てみたいなものをあまり信用していないという感じ.

ScalikeJDBCでは,こういう雰囲気でSQLを発行できる(公式ページ*1より引用).

// insert initial data
Seq("Alice", "Bob", "Chris") foreach { name =>
  sql"insert into members (name, created_at) values (${name}, current_timestamp)".update.apply()
}

// for now, retrieves all data as Map value
val entities: List[Map[String, Any]] = sql"select * from members".map(_.toMap).list.apply()

ScalikeJDBCは,MySQLの他にPostgreSQLやH2などに接続できる.ドライバは各社が作っているので,これをあらかじめsbtで依存性として導入しておけば勝手に良い感じに接続してくれる. 今回はdocker-composeで起動したMySQLサーバに接続する.

ScalikeJDBCのセットアップ

まずはScalikeJDBCがMySQLに接続するための準備をしよう.基本的に公式ページの手順に従いつつ,MySQLに接続する上で不要な箇所を削り,また見落しがちなポイントを追加した.

依存性の導入

ScalikeJDBCは,他のScala/Javaパッケージと同様に,Mavenリポジトリで提供されている.

# build.properties
sbt.version=1.4.5

build.sbtでパッケージの依存性を記述した.

// build.sbt
scalaVersion := "2.13.4"

/* 略 */

libraryDependencies ++= Seq(
  /* 略 */
  // DB
  // MySQLコネクタ.これがないとscalikeJDBCはMySQLに接続できない
  "mysql" % "mysql-connector-java" % "8.0.22",
  // ScalikeJDBC本体
  "org.scalikejdbc" %% "scalikejdbc" % "3.5.0",
  // テスト用にh2 DBを使えるようにしておく(この記事では使わない)
  "com.h2database" % "h2" % "1.4.200", // for test purpose
  // ScalikeJDBCが要求するログフレームワーク
  "ch.qos.logback" % "logback-classic" % "1.2.3",
  // テスト時にいろいろ助けてくれるらしいパッケージ
  "org.scalikejdbc" %% "scalikejdbc-test" % "3.5.0" % "test",
  // 今回はapplication.confに接続・コネクションプールの設定を記述する.
  // それを読み取るためのパッケージ
  "org.scalikejdbc" %% "scalikejdbc-config" % "3.5.0"
)

ここまで記述できたら,sbtを起動して依存性が解決されるか確認すると良いだろう.

application.confに接続設定・コネクションプール設定を記述する

ScalikeJDBC自体のセットアップが終わったので,接続やコネクションプールに関する設定を記述していく.今回はsrc/main/resources/application.confにこの設定を記述できるようにしてあるので,そこに記述する.

# application.conf
# JDBC settings
db.default.driver="com.mysql.jdbc.Driver"
db.default.url="jdbc:mysql://db/データベース名"
db.default.user="ユーザ名"
db.default.password="パスワード"

# Connection Pool settings
db.default.poolInitialSize=5
db.default.poolMaxSize=7
# poolConnectionTimeoutMillis defines the amount of time a query will wait to acquire a connection
# before throwing an exception. This used to be called `connectionTimeoutMillis`. 
db.default.poolConnectionTimeoutMillis=1000
db.default.poolValidationQuery="select 1 as one"
db.default.poolFactoryName="commons-dbcp2"

コネクションプールはあまり興味がなかったので公式ページの記述に従った.今はdriver, url, user, passwordがきちんと書けていればよい.

MySQLの設定

この項はdocker-composeを開発に使い,コンテナとしてMySQLをセットアップするという前提で書かれているので,既にMySQLをセットアップできているなら読み飛ばしましょう.

ここではdocker-composeがMySQLコンテナを起動できるようにし,ScalikeJDBCが接続できるようになるまでのブートストラップができるようにする. おおまかに,「コンテナ自体の設定」「初期化スクリプトの用意」を行う.

コンテナの設定

今回はMySQL 8.0.22を使う.docker-compose.ymlに以下のように記述する.

version: '3'
services:
    # ...略...
    db:
        image: mysql:8.0.22
        volumes:
            - ./tmp/db:/var/lib/mysql
            - ./db:/db
        ports:
            - 3306:3306
        environment: 
            - MYSQL_ROOT_PASSWORD=my-secret-pw
        command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

ここでは以下の設定を行っている.

  • MySQL 8.0.22イメージを使うこと
  • MySQLがデータを保存するディレクトリをBind mountして永続化すること
    • MySQLは/var/lib/mysqlにデータを保存するので,これをホストの./tmp/dbと同期させる.
  • 初期化用SQLスキーマのあるディレクトリをBind mountして読み込めるようにすること
    • スキーマは後述する.スキーマは./db/に保存する.
  • ポートを解放すること
  • MySQLのrootユーザのパスワード
    • ローカルで開発用に使うので適当に設定
  • デフォルトのパスワード認証の方法の指定(標準のcaching_sha2_passwordだとたまに対応していないライブラリがあるので・・・)
  • エンコーディングまわりの設定
    • 最もアンパイな設定

ここまで書けたら,前もって./db/./tmp/db/とをmkdirで作成しておき,docker-compose up dbを実行してDBが立ち上がるか確認しよう.

初期化スクリプトの用意

さて,開発の準備は整ったのですが,スクリプト一発でスキーマが反映されていると嬉しいですね.手でスキーマを導入していくのは苦行です.

そこでDB初期化用のスクリプトをscript/setup_db.shに用意します.

#!/bin/bash
set -ex

MYSQL_HOST=db
MYSQL_USER=root
# cf. docker-compose.yml
MYSQL_PASS=my-secret-pw
# cf. docker-compose.yml
MYSQL_SCHEMA_PATH=/db/schema.sql
docker-compose run --rm db bash -c "mysql -h $MYSQL_HOST -u $MYSQL_USER --password=$MYSQL_PASS < $MYSQL_SCHEMA_PATH"

やっていることはあまり大したことはありません.MySQLへの接続を確立し,スキーマファイルを流し込むだけです.

スクリプトができたら,スクリプトが読み込むためのSQLスキーマファイルをdb/schema.sqlに用意します.

-- Initial Sequence

DROP DATABASE IF EXISTS データベース名;
CREATE DATABASE データベース名 CHARACTER SET utf8mb4 COLLATE = utf8mb4_general_ci;

-- In mysql 8 or higher, creating user and granting should be separated.

-- Drop user

DROP USER IF EXISTS 'readwrite'@'%';
DROP USER IF EXISTS 'readwrite'@'localhost';

-- Prepare user

CREATE USER 'readwrite'@'%'         IDENTIFIED BY 'readwrite';
CREATE USER 'readwrite'@'localhost' IDENTIFIED BY 'readwrite';

GRANT SELECT, UPDATE, INSERT, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON データベース名.* TO readwrite@'%';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON データベース名.* TO readwrite@'localhost';

FLUSH PRIVILEGES;

-- Table Definition

SET sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY';

-- ここにテーブル定義を書いていく

このスキーマでは以下のことをやっています.

  • DBが存在していれば破壊し,新たにDBを作成する
  • ユーザreadwriteがいれば破壊し,新たにユーザを作成する
    • 開発用なので,readwriteというユーザをreadwriteというパスワードで作成しています.
  • ユーザreadwriteに権限を付与する
    • 通常のアプリケーションの動作で必要な権限は網羅しているはずです.
  • 各種設定を行う
    • いわゆるkamipo traditionalです
    • session開始時にも同様の設定をやったほうがよいが,ScalikeJDBCでどうやったらセッション開始時にSQLを発行できるようになるか分からなかったのでスキップ

DBセットアップを実行する

DBコンテナとセットアップスクリプトの設定ができた.さっそく実行してみよう.

まずDBコンテナを起動しておく.

$ docker-compose up db

そのターミナルは開きっぱなしにして,別ターミナルからセットアップスクリプトを実行する.

$ ./script/setup_db.sh

するとDBコンテナのDBがいったん削除され,スキーマファイルの通りにDBがセットアップされます.テーブルが増えるたびにこれを実行すればよいです.

ScalikeJDBCから接続してみる

せっかくなのでScalikeJDBCから接続できることを確認してみましょう.テーブルはまだ作成していないので,テーブルがなくても動くクエリを使います.

import scalikejdbc._

// Setup connection-pool regards to application.conf.
// cf. application.conf.
scalikejdbc.config.DBs.setupAll()

// test
val value = DB readOnly { implicit session =>
  sql"select 1 as one".map(_.long(1)).list.apply()
}
println(s"DB Connection has been established: $value")

このコードを実行すると,うまくMySQLに接続できたならば,DB Connection has been established: 1と表示されるはずです.おめでとう!

自分はCake Patternを採用したScalatraプロジェクトでDBに接続したかったので,このコードをDataBase traitとして,Cake Patternのコンポーネントとしてmix-inしました.

class App
    extends Database {
  /* ... */
}

おわり

今回は次のことについて説明しました:

  • ScalikeJDBCとは
  • ScalikeJDBCの導入
  • 手元のローカル開発環境用のMySQL環境のセットアップ
  • 初期化スクリプトの用意

わからなかったこともありました:

  • セッション開始時に自動的にkamipo traditionalを設定する方法
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?