追記(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に依存しているわけではないのに、Table
やTableQuery
がPostgresDriver
によって生成されるものであるため、 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
には依存するが、それは実行時に決まる」という形で宣言する。
trait Profile {
def profile: JdbcProfile
}
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
のインスタンスを提供するコンポーネントが別に必要になる。
例えば、ドライバのクラス名を提供できるようにすればシンプルになる、などの工夫は必要かと思います。
trait Profile {
def profile: JdbcProfile
def driverClassName: String
}
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でうまく分離することで一つ一つの役割を明確にしたり、あるいはモックで置き換えることができるようになるので便利です。
欠点としてはこれらの仕組みを理解するのに時間がかかることと、クラスが増えてしまうことくらいでしょうか。