読者です 読者をやめる 読者になる 読者になる

Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

Migrating to ScalikeJDBC 2.4.x

ScalikeJDBCは2.4系になってさらに便利になりました。 この記事では、2.3系のユーザを対象におもな変更点を紹介します。

みなさん初めましてっ!

@ocadarumaと申します、おもにSpark/Redshift/DynamoDBなど用いてプロダクトの集計周りを担当してます。

最近はガルパン劇場版のBlu-rayを見るためにPS4を買おうか悩んでます。

Blu-rayのためだけに買うのはもったいないので、おすすめのゲームあったら教えてください!

ScalikeJDBCについて

Scalaエンジニアのみなさん、DBライブラリは何を使ってますか?

弊社のScalaプロジェクトでは、RDBだけでなく、TreasureDataやRedshift等のDWHへの接続まで、大部分でScalikeJDBCを利用しています。

生SQLとの対応がわかりやすくて学習コストが低く、かつ複雑な集計要件にも対応可能なところがよいです。

またメンテナに日本人の方が多く、サポートが手厚いのも嬉しい点ですね。

ScalikeJDBC 2.4.xについて

2016年5月にリリースされたマイナーバージョンアップとなるScalikeJDBC 2.4.0では、以下のようにバイナリ互換を保たない大きめの変更が入っています。

  • ParameterBinderFactoryが導入された
  • DBのTimeZoneを指定可能になった (ちなみに私が出したPR#488で入った機能です:-D)

というわけでこの記事では、ScalikeJDBC 2.3.xのユーザを対象に、上記2点の概要をご紹介します。

環境

  • JDK 1.8
  • Scala 2.11
  • ScalikeJDBC 2.4.2

ParameterBinderFactory

2.3.xでは、QueryDSLを用いてPreparedStatementにパラメータをセットする際にうっかり独自型をそのまま書いてしまう、なんてミスがよくありました。

import scalikejdbc._

// 独自型
case class UserId(value: Long) extends AnyVal

case class User(id: UserId, name: String)

object User extends SQLSyntaxSupport[User] {
  val u = this.syntax("u")
  
  def apply(u: SyntaxProvider[User])(rs: WrappedResultSet): User = autoConstruct(rs, u)

  def findById(id: UserId)(implicit session: DBSession): Option[User] = {
    withSQL {
      selectFrom(User as u)
        .where.eq(u.id, id) // idは独自型なので、PreparedStatement#setObjectされてしまう!
    }.map(this(u)).single().apply()
  }
}

正しくはwhere.eq(u.id, id.value)のようにする必要がありますが、コンパイル時にこのミスを検出できなくてつらみがありますね。

2.4.0で、eqのシグネチャがeq[B: ParameterBinderFactory](column: SQLSyntax, value: B): ConditionSQLBuilder[A]となり、BParameterBinderFactory型クラスインスタンスを要求するようになりました。(もちろんeqは一例で、API全体がそうなっています。)

したがって2.4.0以降では、↑のコードは無事コンパイルエラーになります!

次のようなimplicit valueを探索スコープ内に置いておけばコンパイルできるようになります。安全ですね!

implicit val userId: ParameterBinderFactory[UserId] = ParameterBinderFactory(id => _.setLong(_, id.value))

ただし、ScalikeJDBC 2.4.2現在、SQLInterpolationはParameterBinderFactoryに対応していません。

よって下記のコードはコンパイルされてしまうので気を付けましょう。

// PreparedStatement#setObjectされてしまう
def findById(id: UserId)(implicit session: DBSession): Option[User] = {
  sql"select ${u.result.*} from ${User as u} where ${u.id} = $id"
    .map(this(u)).single().apply()
}

DBのTimeZone指定

TimeZone関連は注意深く作らないとバグを生みやすい箇所です。

たとえばRedshiftなど、TimeZoneが固定(UTC)で、Zone付きの時刻型が存在しないようなDBに接続するアプリケーションを作る場合、

  • DBへUNIX timeで時刻を格納する
  • もしくはクエリの際にTimeZoneを変換するSQL関数をかませる
  • もしくはアプリを、DBと同じTimeZoneで動かす前提で作る

などの措置をとらないと、アプリとDBのTimeZoneがズレている場合に、正しくクエリが行えないなどの問題が発生します。

こういった場合、DBライブラリ側で接続先DBのTimeZoneに合わせる機能があると便利です。

ScalikeJDBC 2.4.0では、ConnectionPoolSettings#timeZoneで、poolごとにDBのTimeZoneを指定できるようになりました。

scalikejdbc-configを使っていれば、application.conf等設定ファイルで指定もできます。

指定すると、JVMのdefault TimeZoneとの間でTimeZoneが変換されるようになります。

少し試してみましょう。

まずはTimeZone設定なしです。設定しなかった場合、アプリのTimeZoneが使われます。

db {
  default {
    driver = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/example"
    user = "root"
    password = "root"
  }
}

テーブル作ります。

mysql> create table zone_sample(id int, time timestamp);

アプリのTimeZoneをJSTにして、SQLInterpolationでinsertしてみましょう。

import java.time.LocalDateTime
import java.util.TimeZone
import scalikejdbc._, config.DBs

DBs.setupAll()

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"))

// 2016年6月10日12時0分
val time = LocalDateTime.of(2016, 6, 10, 12, 0)

DB autoCommit { implicit session =>
  sql"insert into zone_sample(id, time) values (1, $time)".execute().apply()
}

以下のように、JSTのまま時刻が格納されてますね。

mysql> select * from zone_sample;
+------+---------------------+
| id   | time                |
+------+---------------------+
|    1 | 2016-06-10 12:00:00 |
+------+---------------------+
1 row in set (0.00 sec)

では今度はDBのTimeZoneをUTCにしてみましょう。

db {
  default {
    driver = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/example"
    user = "root"
    password = "root"
    timeZone = UTC // DBのTimeZoneを指定
  }
}
// 2016年6月10日12時0分
val time2 = LocalDateTime.of(2016, 6, 10, 12, 0)

DB autoCommit { implicit session =>
  sql"insert into zone_sample(id, time) values (2, $time2)".execute().apply()
}

DBを確認してみると、UTCとして時刻が格納されていますね!

mysql> select * from zone_sample;
+------+---------------------+
| id   | time                |
+------+---------------------+
|    1 | 2016-06-10 12:00:00 |
|    2 | 2016-06-10 03:00:00 |
+------+---------------------+
2 rows in set (0.00 sec)

まとめ

今回ご紹介した機能含め、ScalikeJDBCはバージョンが上がってさらに使いやすくなっています。

周りに、Slickで消耗している方や、doobieみたいにモナってるのはちょっと...という方がいたら是非お勧めしていきましょう。

それではごきげんよう、Enjoy ScalikeJDBC !