20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SlickでJDBCドライバを実行時に切り替えるメモ(ver. 2.x)

Last updated at Posted at 2014-06-25

追記(2015/06/02): Slick ver 2.xで試したメモです。ver 3.xでは互換性のない変更があるため、同じコードは動作しません。

とあるコードで見かけたので、自分自身のためにメモ。

Slick公式のコードサンプル : https://github.com/typesafehub/activator-slick-multidb

やりたいこと

稼働時は PostgreSQL に接続するけど、単体テスト時は H2Database に接続してCRUDとかその上のサービスクラスの挙動をテスト記述したい時がある。

Slickのサンプルにもあるように通常はデータベースを選んで、その上でテーブル設計を作る。以下はPostgreSQLの場合。

import scala.slick.driver.PostgresDriver.simple._

// データモデル
case class User(id: Int, name: String)

// テーブル定義
class UserTable(tag: Tag) extends Table[User](tag, "users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def * = (id, name) <> (User.tupled, User.unapply) 
}

// クエリ実行オブジェクト
val Users = TableQuery[UserTable]

でも使う段になって、上記コードの中身は全くPostgreSQLに依存しているわけではないのに、TableTableQueryPostgresDriverによって生成されるものであるため、 H2Driver に切り替えることは出来ない。

import scala.slick.driver.H2Driver.simple._

Database.forURL("jdbc:h2:mem:test", driver = "org.h2.Driver") withSession { implicit session =>
  // 定義時とドライバが合わないのでコンパイルエラー
  Users.insert(User(-1, "sample")) 
}

Slick 2.x にはいくつかのProfileが設定されていてざっくりとは以下の様な階層になっているようだ。
上記でimportしているのはobjectに定義されたsimple以下を使っている。(ここに暗黙の型変換とか定義されてる)ちなみに、objectは同名のクラスをシングルトンインスタンスにしたもの。

JdbcProfile -> JdbcDriver(class) -> PostgresDriver(class), H2Driver(class), ...
                 JdbcDriver(object)   PostgresDriver(object)  H2Driver(object) ...            

じゃあ、 例えば上位クラスの JdbcDriver を定義時にセットして実行時に PostgresDriver を利用していいかというと、やはり同じく実行時のドライバ指定ができない。

そこで、実行時に依存性注入(DI)することになる。

テーブル定義のComponent化

DIでは、「何らかの JdbcProfile には依存するが、それは実行時に決まる」という形で宣言する。

Profile.scala
trait Profile {
  def profile: JdbcProfile
}
UserTable.scala
trait UserTableComponent { this: Profile =>
  import profile.simple._

  // UserTableの定義を内包
}

このように定義しておくことで、どのProfileが使われるかは実行時に決めることができる。

trait H2Profile extends Profile {
  val profile = H2Driver
}

trait PostgresProfile extends Profile {
  val profile = PostgresDriver
}

そして、実行時に UserTable を使うクラスは、 UserTableComponent をmix-inして使うように宣言する。

class SomeService { this: UserTableComponent =>
  // UserTableの処理
}

// サービスクラスのインスタンス化
object SomeService extends SomeService with UserTableComponent with PostgresProfile

DBコネクションのComponent化

上記の説明で足りない部分があり、それはずばりDBへの接続の部分。

  • Database.forURL のように呼び出したくても、SomeServiceクラスでは実際には Database オブジェクトのメソッドは呼び出せない。(Profileを持ってないから)
  • Slickの説明にもある通り、コネクションプーリングは機能に含まれていないので、コネクションプーリングライブラリから生成された DataSource を使うようにする。
  • でも、テスト時はコネクションプーリングは要らない。

ということで、 Database のインスタンスを提供するコンポーネントが別に必要になる。
例えば、ドライバのクラス名を提供できるようにすればシンプルになる、などの工夫は必要かと思います。

Profile.scala
trait Profile {
  def profile: JdbcProfile
  def driverClassName: String
}
Database.scala
trait DatabaseComponent { this: Profile =>
  def database: profile.simple.Database
}

// テスト時
trait H2DatabaseComponent extends DatabaseComponent {
  import profile.simple._

  def database = Database.forURL("jdbc:h2:mem:test")
}

// 稼働時など
trait DbcpBasicDatabaseComponent extends DatabaseComponent {
  import profile.simple._
 import org.apache.commons.dbcp2.BasicDataSource

  def database = {
    val ds = new BasicDataSource
    ds.setDriverClassName(driverClassName)
    // ...
    Database.forDataSource(ds)
  }
}

あえて省略していますが、実際にはdatabaseメソッドに引数を設定するか、別のConfigをmix-inするなどして接続先情報を定義する必要があります。

DI, DI, またDI

ということで、結果的に以下のものを実際のサービスから分離して、実行時にmix-inすることになります。

  • JdbcProfile: Slickで定義された各種RDBMS用のドライバ(DDLとかクエリの差分とかよしなに生成してくれる)
  • コネクション: 普通に接続するとか、DBCPやBoneCPなどでプーリングしたものから取得するとか。
  • 接続先情報: これはそもそも実行時にしか決まらないもの。設定情報を提供する Config などのコンポーネントを使うことになる。

実行するためには絡めなきゃいけない情報でも、DIでうまく分離することで一つ一つの役割を明確にしたり、あるいはモックで置き換えることができるようになるので便利です。

欠点としてはこれらの仕組みを理解するのに時間がかかることと、クラスが増えてしまうことくらいでしょうか。

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?