ScalikeJDBC のバルクインサートやバッチ更新よもやま
前書き
この記事は @kijuky さんの ScalikeJDBCでバルクインサート&バルクアップデート
という記事に触発されて書きました。
上記記事では主に SQL Interpolation での解決をされていらしたので、QueryDSL を使った時のヘルパーについて紹介します。
この記事で使用する言語とライブラリのバージョンは以下の通りです。
- Scala 3.3.1
- ScalikeJDBC 4.0.0
JDBC のバッチ更新API
JDBC ではもともとバルクインサート等とは別に複数の更新操作を一度に行うための機能を備えています。
ScalikeJDBC は JDBC の自然なラッパーであるため、当然この API も呼び出すことができます。
import scalikejdbc._
DB localTx { implicit session =>
val batchParams: Seq[Seq[Any]] = (2001 to 3000).map(i => Seq(i, "name" + i))
sql"insert into employee (id, name) values (?, ?)".batch(batchParams: _*).apply()
}
PostgreSQL などのバッチ更新に対応している RDBMS を使う場合、バルクインサート等の代わりにこちらの API も検討してみるとよいかもしれません。
MySQL Connector/J の rewriteBatchedStatements
話がややこしいのは MySQL を利用している場合ですね。
MySQL では単純にバッチ更新API を利用するとバルクインサート等に比べて極端にパフォーマンスが劣化する場合があります。
そのためバッチ更新API を使わずにバルクインサート等を使いたいというケースが度々発生します。
もちろんコードでバルクインサート等を書いてもいいのですが、なんと MySQL の JDBC Driver である MySQL Connector/J ではバッチ更新APIで呼び出されたクエリ内容を裏側でバルクインサート等に書き換えるという機能が存在しています。
この機能を使うためのプロパティが rewriteBatchedStatements
です。
なかなか豪快な機能ですね。もちろんトレードオフもあるので常に使えるとは限りませんが、状況に応じて利用を検討してもいいかもしれません。
QueryDSL を使った方がよい場合
SQL Interpolation で問題ない場合は SQL Interpolation をそのまま使ってしまえばいいのですが、アプリケーションが複雑になってくると QueryDSL を使った方がよいケースというのも出てきます。
その最たる例がドメインプリミティブを使うケースですね。
例えば Employee
エンティティの Id や Name に、JDBC が直接扱える Int
や String
などではなくアプリケーションで独自の型を定義している場合を考えてみましょう。
case class EmployeeId(value: Int)
case class EmployeeName(value: String)
case class Employee(id: EmployeeId, name: EmployeeName)
これを SQL Interpolation で扱うためにはきちんと JDBC が扱えるデータ型に明示的に変換する必要があります。
val emp: Employee = ...
DB localTx { implicit session =>
sql"insert into employee (id, name) values (${emp.id.value}, ${emp.name.value})"
.update.apply()
}
明示的に .value
を呼び出す必要がある訳ですね。
そして SQL Interpolation の制約上、この .value
呼び出しをうっかり忘れてしまってもコンパイルは通ります。
val emp: Employee = ...
DB localTx { implicit session =>
// ミスしてもコンパイルエラーにならない!
sql"insert into employee (id, name) values (${emp.id}, ${emp.name})"
.update.apply()
}
その結果、実行時にエラーになったりもっと悪いケースではエラーにならずに意図と異なる値がDBに格納されたりします。困ってしまいますね。
そしてこれを防ぐ仕組みが QueryDSL には存在しています。それが ParameterBinderFactory
という型クラスですね。
import scalikejdbc._
given ParameterBinderFactory[EmployeeId] =
summon[ParameterBinderFactory[Int]].contramap(_.value)
given ParameterBinderFactory[EmployeeName] =
summon[ParameterBinderFactory[String]].contramap(_.value)
この様にそれぞれのドメインプリミティブに対し ParameterBinderFactory
を定義しておくと、以下のような QueryDSL で .value
のような変換コードを書く必要がなくなります。
val emp: Employee = ...
val column = Employee.column
DB localTx { implicit session =>
withSQL {
insert.into(Employee).namedValues(
column.id -> emp.id, // .value が不要
column.name -> emp.name
)
}.update.apply()
}
また、仮に ParameterBinderFactory
を定義しなかった場合、上記コードはコンパイルエラーになります。安心安全ですね。
BatchParamsBuilder
そして、上記の ParameterBinderFactory
をバッチ更新API で利用するためのヘルパーとして BatchParamsBuilder
というクラスがあります。
val emps: Seq[Employee] = ...
val column = Employee.column
val builder = BatchParamsBuilder {
emps.map { emp =>
Seq(
column.id -> emp.id, // ここで ParameterBinderFactory が使われる
column.name -> emp.name // 見つからなければコンパイルエラー
)
}
}
DB localTx { implicit session =>
withSQL {
insert.into(Employee).namedValues(builder.columnsAndPlaceholders: _*)
}.batch(builder.batchParams: _*).apply()
}
これを利用することで、ドメインプリミティブを活用するような規模のアプリケーションでも安全にバッチ更新API を利用することができます。
ScalikeJDBC にて QueryDSL でバッチ更新API を使いたい場合には BatchParamsBuilder
の利用も検討してみてください。
Discussion