来世から頑張る!!

技術ブログを目指して

ScalaでゆるふあにDB接続


この記事はScala Advent Calendar 2015 12日目です。

www.adventar.org

昨日は同じく私による今日から始めるScalaプログラミングでした。

前置き

私はアプリケーションにSQLを自分で書きたい派です。
参考: ScalaでSQLが書きたいんだ!!

なので積極的に自分で書けるライブラリーが好きです。

  1. DBに直接繋いで、取得結果や処理速度などを調整するクエリーを書く。
  2. 出来上がったクエリーを作成するためのScalaコードを書く。

こんな風に作業したいのです。

doobie or not DB

ということでdoobieです。

doobieによるシンプルなDBアクセスコードは以下のような感じです。

import doobie.imports._

case class User(id: String, password: String, email: Option[String])

object User {
  def byId(id: String): ConnectionIO[Option[User]] =
    sql"SELECT id, password, email FROM users WHERE id = ${id}".query[User].option
}

呼び出す側はこちら

import doobie.imports._
import scalaz.effect.IO

val xa = DriverManagerTransactor[IO](
  "com.mysql.jdbc.Driver", "jdbc:mysql://example.com:3306/example", "user", "password"
)
val a: ConnectionIO[Option[User]] = User.byId("kazzna")
val b: IO[Option[User]] = a.transact(xa)
val c: Option[User] = b.unsafePerformIO

これの素晴らしいところは、接続情報がxaにまとめられていて、それを明示的に渡すということですね。

ちなみにDriverManagerTransactorの型引数はscalaz.Catchableを要求してきたりするので、 IO以外ではscalaz.concurrent.Taskぐらいしか使える型を知りません。
(きっとガチ勢がコメントしてくれるはず・・・)

テストを書いてみる

最初に書いた通り、SQLが正しいことは確認してからScalaコードを書きたいので、 ユニットテストでDBになんて繋ぎたくないんですね。

ユニットテストで確認するのは「使用したいSQLが出力されているか」までで、 外部との接続はもうちょっと結合度の高いテストでやりたいわけです。

ですが世間一般のライブラリーはいつの間にかDB接続が暗黙的に渡されていたりして、 テストしようと思うと色々なテクニック(モックとか)を使わないと難しかったりします。

ところがこのdoobieはゆるふあ仕様で、それが簡単にできるのです!!!

val a = User.byId("kazzna") // ConnectionIO[Option[User]]
val b = a.transK[IO] // Kleisli[IO, java.sql.Connection, Option[User]]

Kleisli[M[_], A, B]とはゆるふあに言えばA => M[B]です。
なのでこの場合はb: java.sql.Connection => IO[Option[User]]ですね。

つまりjava.sql.Connectionを渡せばテストできるわけです。
このinterfaceを実装しましょう。

import java.sql._

import org.scalatest._

class QueryChecker(sql: String, params: Map[String, Any], result: ResultSet) extends Connection with Matches {
  override def prepareStatement(s: String): PreparedStatement = new PreparedStatement {
    s should ===(sql)
    override def setString(i: Int, s: String): Unit = {
      params(i) should ===(s)
    }
    override def executeQuery(): ResultSet = result
    // override def ... = ??? を必要なだけ書く
  }
  // override def ... = ??? を必要なだけ書く
}

class DummyResultSet(v: List[Map[Int, Any]]) extends ResultSet {
  var rowIndex = -1
  override def next(): Boolean = {
    rowIndex = rowIndex + 1
    rowIndex < v.length
  }
  override def getString(i: Int): String = v(rowIndex)(i).asInstanceOf[String]
  // override def ... = ??? を必要なだけ書く
}

気を付けるべきことは、import java.sql._java.sql.Arrayがimportされるため、 scala.Array[A]Arrayと書けないことです。
scala.Arrayと全部ちゃんと書く必要があります。もしくは別名import。

あと、closeとかもoverrideしないとランタイムエラーが出ると思いますので、 エラーが出たところから修正していきましょう。

なお、このあたりのコードはJava用なのでmutableに書いたほうが楽だと思われます。

import org.scalatest._

class UserSpec extends FlatSpec with Matches {
  "Sql" should "be checked by test Connection" in {
    val id = "kazzna"
    val query = "SELECT id, password, email FROM users WHERE id = ?"
    val map = Map(1 -> id, 2 -> "pass", 3 -> "email@example.jp")
    val expected = Some(User(map(1), map(2), Some(map(3))))
    val a = User.byId(id).transK[IO]
    val b = new QueryChecker(query, Map(1 -> id), new DummyResultSet(Seq(map)))
    val actual = a(b).unsafePerformIO
    actual should ===(expected)
  }
}

これでDB接続情報を一度も書かずにユニットテストができました!!簡単!!

例によって例の如くQueryにはCypherを使用していますが、 一応動くサンプルをgithubにあげておきました。

github.com

最後に: 教えて怖い人!

ConnectionIOOptionT[ConnectionIO, A]EitherT[ConnectionIO, String, A]なんかにして、 2つ3つ組み合わせて途中で失敗したら実行をやめるようなqueryを書くときに型引数つらいのですが、 なんとかなる方法はありませんか?

コメントお待ちしております(>﹏﹏<);;

以上でScala Advent Calendar 2015 12日目は終了です。
お付き合いいただきありがとうございます。

明日はOE_uiaさんによる「Scala AndroidかDeepLearning的な何か」です。
レベルが高そうですが、楽しみです。