ScalaでMySQLを操作する際のプロジェクトの設定や解説がプロジェクト内やメンバー内のどこにも残ってないので備忘録がてらまとめてみた記事になります。
前提:scalikejdbcを使う
ScalaでSQLを操作するライブラリは色々あるみたいですが、プロジェクト内ではscalikejdbcが利用されており、Scala3にも対応されているっぽいので、この記事ではこのscalikejdbcを使うことを前提にします。
プロジェクト設定
scalikejdbcを使うためにDependencies.scalaに必要なライブラリを追加します。
lazy val scalaLikeJdbcDependencies: Seq[ModuleID] = Seq(
"org.scalikejdbc" %% "scalikejdbc" % "3.5.0", // scalaikejdbc本体
"org.scalikejdbc" %% "scalikejdbc-config" % "3.5.0", // データベースと接続するためのConfig
"mysql" % "mysql-connector-java" % "5.1.29" // mysqlに繋ぐために必要JDBCドライバ
)
ローカルにMySQLを構築する
dockerを使用してローカルにMySQLを構築します。
設定の詳細は割愛しますが、公式の説明と色々なQiitaを読みながらこの設定だけあれば
必要最低限のテストが動くだろうみたいな感じの設定です。
https://hub.docker.com/_/mysql
https://qiita.com/Manabu-man/items/58d0f98a15656ed65136
version: '3'
services:
mysql:
image: mysql:5.7
container_name: mysql_host
environment:
MYSQL_ROOT_PASSWORD: root # 管理者パスワード
MYSQL_DATABASE: test_database # イメージ起動時に独自のデータベースを作っておく設定
MYSQL_USER: docker
MYSQL_PASSWORD: docker
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- ./docker/myqsl/my.cnf:/etc/mysql/conf.d/my.cnf # MySQLの設定を読み込む(文字コードとか)
ports:
- 3306:3306 # 3306ポートで接続できるようにする
ディレクトリ構成は下記のように配置して、my.cnfを以下に置いております。
├ [docker]
| └ [mysql]
| └ my.cnf
├ docker-compose.yaml
下記コマンドでdockerを立ち上げます。
docker-compose up
別ターミナルで立ち上げたMySQLにログインして、作成したデータベースが表示されていれば構築できてるはずです。
1. コンテナのIDを調べる
docker ps
2. コンテナに入る
docker exec -it {コンテナID} bash
例)
docker exec -it f5c91af3d25c bash
3. コンテナからmysqlに入る
mysql -u root -p
4. データベース一覧確認
show databases;
test_databaseが表示されていればOK
5. データベース切替
use {データベース名}
例)
use test_database
ScalaからMySQLに接続する
あとは下記のようなテストコードを実装して接続できれば、色々scalalikejdbcを試せる環境が構築できたはずなので、実際に色々やってみましょう!
import org.scalatest.wordspec.AnyWordSpec
import scalikejdbc._
import scalikejdbc.config.DBs
import java.sql.DriverManager
import scala.util.{Failure, Success, Try}
class RdsWrapperSpec extends AnyWordSpec {
"テスト" when {
"hogeテーブルを作成" should {
"作成できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) localTx { implicit session =>
hoge("hoge")
}
}
def hoge(tableName: String)(implicit session: DBSession): Unit = {
(for {
_ <- createTable(tableName)
} yield ()) match {
case Success(_) =>
session.connection.commit()
println("success")
case Failure(e) =>
session.connection.rollback()
println(e)
}
}
def createTable(tableName: String)(implicit session: DBSession): Try[Boolean] = Try {
SQL(
s"""
|CREATE TABLE IF NOT EXISTS $tableName (hello varchar(100))
""".stripMargin
).execute().apply()
}
}
}
trait WithFixture { }
}
備忘録
環境構築の話は以上でおわりでここからは自分用の備忘録になります。
コネクション管理
DB コネクション管理のやり方は 2 通りあります。
DriverManager を使う
java.sq.DriverManager を使うやり方は最もシンプルな方法です。
"driverManagerテーブルを作成(driverManager)" should {
"作成できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
using(DB(DriverManager.getConnection("jdbc:mysql://localhost:3306/test_database", "root", "root"))) { db =>
db localTx { implicit session =>
hoge("driverManager")
}
}
}
}
コネクションプールを使う
DriverManager よりも scalikejdbc.ConnectionPool を使うことを推奨されるみたいです。ConnectionPool は、デフォルトでは Apache Commons DBCP で実装されています。
"ConnectionPoolでテーブルを作成(driverManager)" should {
"作成できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.singleton("jdbc:mysql://localhost:3306/test_database", "root", "root")
using(DB(ConnectionPool.borrow())) { db =>
db localTx { implicit session =>
hoge("connection_pool")
}
}
}
}
もし複数のデータソースに接続する必要があれば、ConnectionPool.add(dbName) を使います。
"Connectionプールで複数でテーブル作成" should {
"作成できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
using(DB(ConnectionPool(Symbol("hoge")).borrow())) { db =>
db localTx { implicit session =>
hoge("piyo")
}
}
}
}
省略形
ただ、上記の例はあまりに冗長なため、DB オブジェクトを使うともっとシンプルになります。
using(DB(ConnectionPool(Symbol("hoge")).borrow())) { db =>
db localTx { implicit session =>
...
↓ 省略形
NamedDB(Symbol("hoge")) localTx { implicit session =>
hoge("hoge")
}
SQL操作
ScalikeJDBC の標準的な使い方は SQL(String).map(f).{output}.apply() という API を利用するスタイルです。
apply メソッドを呼び出すまでは SQL は実際にはまだ実行されていません。つまり SQL の apply メソッドよりも前の部分については常に再利用することができます。
readOnly ブロック / セッション
クエリをリードオンリーモードで実行します。
"SQL(Select)操作" should {
"操作できる" in new WithFixture {
case class Foo(hello: String)
val * = (rs: WrappedResultSet) => Foo(rs.string("hello"))
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.singleton("jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) readOnly { implicit session =>
// Query
val all = SQL("select * from foo").map(*).list().apply()
print(all)
}
}
}
// 結果
List(Foo(hoge), Foo(hoge))
更新処理をリードオンリーモードで実行すると java.sql.SQLException が発生します。
NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("update emp set name = ? where id = ?").bind("foo", 1).update.apply()
} // java.sql.SQLException が throw される
autoCommit ブロック / セッション
クエリや更新をオートコミットモードで実行します。
JDBC 自動コミット・モード
デフォルトでは、JDBC は自動コミットと呼ばれる操作モードを使用します。 このモードでは、データベースに対するすべての更新が即時に永続的にコミットされます。
ただし、自動コミット・モードは、 作業論理単位がデータベースに複数の更新を必要とする状況では、安全性に欠けます。 自動コミット・モードを使用した場合、1 つの更新が行われてから他の更新が行われるまでの間にアプリケーションやシステムで何らかの問題が発生すると、最初の更新は元に戻せなくなります。
"クエリをautoCommitで実行" should {
"実行できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) autoCommit { implicit session =>
SQL("create table company (id integer primary key, name varchar(30))").execute().apply()
}
}
}
// mysql> show tables;
+-------------------------+
| Tables_in_test_database |
+-------------------------+
| company |
+-------------------------+
localTx ブロック
クエリや更新をブロックのスコープに閉じた同一トランザクションで実行します。
ブロック内で例外が throw された場合、自動的にトランザクションはロールバックされます。
"クエリをlocalTxで実行" should {
"実行できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) localTx { implicit session =>
SQL("insert into company values (?, ?)").bind(1,"Typesafe").update().apply()
}
}
}
//mysql> select * from company;
+----+----------+
| id | name |
+----+----------+
| 1 | Typesafe |
+----+----------+
withinTx ブロック / セッション
クエリや更新を既に存在しているトランザクション内で実行します。
トランザクションについての操作(Tx#begin()、 Tx#rollback() や Tx#commit())はすべてライブラリ利用者によって制御される必要があります。
"クエリをwithinTxで実行" should {
"実行できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
myWithinTx(DB(ConnectionPool(Symbol("hoge")).borrow())) { implicit session =>
SQL("insert into company values (?, ?)").bind(4, "Typesafe").update().apply()
}
}
def myWithinTx[A](db: DB)(f: DBSession => A): A = {
using(db) { db =>
try {
db.begin()
val result = db withinTx { implicit session =>
f(session)
}
result match {
case Failure(_) => db.rollback()
case Left(_) => db.rollback()
case _ => db.commit()
}
result
} catch {
case e: Throwable =>
println(e)
db.rollback()
throw e
} finally {
db.close()
}
}
}
オペレーション
Query API
scalalikejdbcはQuery API としていくつかの API を提供しています。
これら(single、 first、 list、 foreach)はすべて java.sql.PreparedStatement#executeQuery() を実行します。
single
single はマッチした単一の行を Option 型に包んで返します。
複数の行がマッチした場合は例外を throw します。
"クエリをsingleで実行" should {
"実行できる" in new WithFixture {
"クエリをsingleで実行" should {
"実行できる" in new WithFixture {
case class Company(id: String, name: String)
val companyMapper = (rs: WrappedResultSet) => Company(rs.string("id"), rs.string("name"))
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
val result = NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("select * from company where id = ?").bind(1).map(rs => Company(rs.string("id"), rs.string("name"))).single().apply()
}
println(result)
// mapperを定義したケース
val result2 = NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("select * from company where id = ?").bind(1).map(companyMapper).single().apply()
}
println(result2)
}
}
// 結果
Some(Company(1,Typesafe))
Some(Company(1,Typesafe))
first
first はマッチした行のうち、最初の行を Option 型に包んで返します。
"クエリをfirstで実行" should {
"実行できる" in new WithFixture {
case class Company(id: String, name: String)
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
val result = NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("select * from company").map(rs => Company(rs.string("id"), rs.string("name"))).first().apply()
}
println(result)
}
}
list
list はマッチした複数の行を scala.collection.immutable.List 型として返します。
"クエリをlistで実行" should {
"実行できる" in new WithFixture {
case class Company(id: String, name: String)
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
val result = NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("select * from company").map(rs => Company(rs.string("id"), rs.string("name"))).list().apply()
}
println(result)
}
}
// 結果
List(Company(1,Typesafe), Company(2,Typesafe), Company(3,Typesafe), Company(4,Typesafe))
foreach
foreach はイテレーション操作(一度に結果全体を読み込まない)の中で副作用を伴う処理を行うことができます。この API は巨大な検索結果に対して処理を行うときに有用です。
"クエリをforeachで実行" should {
"実行できる" in new WithFixture {
case class Company(id: String, name: String)
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) readOnly { implicit session =>
SQL("select * from company").foreach(rs => println(rs.string("id"), rs.string("name")))
}
}
}
//結果
(1,Typesafe)
(2,Typesafe)
(3,Typesafe)
(4,Typesafe)
Update API
update は java.sql.PreparedStatement#executeUpdate() を実行します。
executeUpdate()
このPreparedStatementオブジェクトのSQL文を実行します。それはSQLデータ操作言語(DML)文(INSERT文、UPDATE文、DELETE文など)であるか、DDL文のような何も返さないSQL文でなければなりません。
https://docs.oracle.com/javase/jp/8/docs/api/java/sql/PreparedStatement.html
"updateAPIで実行" should {
"実行できる" in new WithFixture {
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.add(Symbol("hoge"), "jdbc:mysql://localhost:3306/test_database", "root", "root")
NamedDB(Symbol("hoge")) localTx { implicit session =>
SQL("insert into company values (?, ?)").bind(2, "Typesafe").update().apply()
}
}
}
参考文献