ScalaでゆるふあにDB接続
この記事はScala Advent Calendar 2015 12日目です。
昨日は同じく私による今日から始めるScalaプログラミングでした。
前置き
私はアプリケーションにSQLを自分で書きたい派です。
参考: ScalaでSQLが書きたいんだ!!
なので積極的に自分で書けるライブラリーが好きです。
- DBに直接繋いで、取得結果や処理速度などを調整するクエリーを書く。
- 出来上がったクエリーを作成するための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にあげておきました。
最後に: 教えて怖い人!
ConnectionIO
をOptionT[ConnectionIO, A]
やEitherT[ConnectionIO, String, A]
なんかにして、
2つ3つ組み合わせて途中で失敗したら実行をやめるようなqueryを書くときに型引数つらいのですが、
なんとかなる方法はありませんか?
コメントお待ちしております(>﹏﹏<);;
以上でScala Advent Calendar 2015 12日目は終了です。
お付き合いいただきありがとうございます。
明日はOE_uiaさんによる「Scala AndroidでかDeepLearning的な何か」です。
レベルが高そうですが、楽しみです。