始めに
Slickの公式ドキュメントって基本使用するデータベース決め打ち前提(import slick.jdbc.H2Profile.api._
等)で書かれてるけど、やっぱり使ってると本番環境はMySQLだけどテストはH2でさらっと済ましたいってことあるじゃないですか。
でもslick.jdbc.H2Profile
にしてもslick.jdbc.MySQLProfile
にしてもslick.jdbc.JdbcProfile
を継承してるのは分かるけど、内部が複雑怪奇でどう手を付けていいのか分からん!!
それでも何とかならんのかって調べてみたところ、こんな記事に当たったので早速、パクらせて参考にさせてもらって書いてみます。
各Profileのコンポーネント化
上記の参考元では、JdbcProfile
を抽象化したコンポーネント(trait)を継承したリポジトリやサービス等を組んでおいて、実装時に各DBのドライバで具象化したコンポーネント(trait)を引き渡してやればいいんだぜって感じで書いてあるんじゃないかと思います・・・たぶん。
とりあえず書けば分かる!!
まず、抽象的なコンポーネントを書いてみます。JdbcProfile
を使用しますからって宣言的なtraitです。
package com.example.infrastructure
import slick.jdbc.JdbcProfile
/** Interface for database component **/
trait DBComponent {
/** Jdbc profile **/
val profile: JdbcProfile
import profile.api._
/** Database **/
def db: Database
}
目からウロコだったのがval profile: JdbcProfile
宣言後のimport profile.api._
です。これによってdef db: Database
を宣言することができます(def db: profile.api.Database
とも書けます)。
変数からimport宣言したり、型を明示したりすることが出来るなんて知りませんでした。
次に上のを継承した各DBドライバ用の具象化コンポーネントを用意します。
package com.example.infrastructure
import com.typesafe.config.ConfigFactory
import slick.jdbc.H2Profile.api._
import slick.jdbc.{H2Profile, JdbcProfile}
/** Database component for H2 **/
trait H2DBComponent extends DBComponent {
/** Jdbc profile **/
override lazy val profile: JdbcProfile = H2Profile
import profile.api._
/** Database instance **/
override lazy val db: Database = H2DBConnector.connection
}
/** Singleton database connector for H2 **/
private object H2DBConnector {
private val config = ConfigFactory.load()
config.checkValid(ConfigFactory.defaultReference(), "slick.h2")
/** Connection **/
val connection = Database.forConfig("slick.h2", config)
}
package com.example.infrastructure
import com.typesafe.config.ConfigFactory
import slick.jdbc.MySQLProfile.api._
import slick.jdbc.{JdbcProfile, MySQLProfile}
/** Database component for MySQL **/
trait MySQLDBComponent extends DBComponent {
/** Jdbc profile **/
override lazy val profile: JdbcProfile = MySQLProfile
import profile.api._
/** Database instance **/
override lazy val db: Database = MySQLDBConnector.connection
}
/** Singleton database connector for MySQL **/
private object MySQLDBConnector {
private val config = ConfigFactory.load()
config.checkValid(ConfigFactory.defaultReference(), "slick.mysql")
/** Connection **/
val connection = Database.forConfig("slick.mysql", config)
}
見ての通りですが、val profile: JdbcProfile
をオーバーライドして各DBのProfileを割り当てています。
今回はH2とMySQLだけにしてますが、他のSlickがサポートしてるDBなら大体同じ感じで書けると思います。
リポジトリ
続いて上記のコンポーネントを使う方法です。参考元では自分型にDBComponent
を割り当てていますが自分型はあんまり好みではないのでDI方式で書いてみます(オリジナリティをちょっとでも出したい)。
まず単純なUserというモデルを作りました。
package com.example.repository
/** Model **/
object Model {
/** User
*
* @param id identity
* @param name name
*/
final case class User(id: Option[Int], name: String)
}
リポジトリを実装します。本当はスキーマ定義は切り出した方が良いとは思いますが、今回は手抜きで。
UserRepository
を継承していますが、これは単なるインターフェースなので載せるのは省略します。
package com.example.repository
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scalaz.\/
import scalaz.syntax.ToEitherOps
import com.example.infrastructure.DBComponent
/** User repository implement dependency injection factory **/
object UserRepositoryImplDI {
def apply(dbComponent: DBComponent): UserRepositoryImplDI = new UserRepositoryImplDI(dbComponent)
}
/** User repository implement dependency injection
*
* @param dbComponent database component
*/
class UserRepositoryImplDI(dbComponent: DBComponent) extends UserRepository with ToEitherOps {
import dbComponent.profile.api._
import Model._
private val users = TableQuery[UserTable]
/** Get all
*
* @return Sequence in user
*/
override def getAll(implicit ec: ExecutionContext): Future[\/[Throwable, Seq[User]]] =
dbComponent.db.run(users.result).map(_.right[Throwable]).recover {
case NonFatal(ex) => ex.left[Seq[User]]
}
/** Get by id
*
* @param id identity
* @return Optional in user
*/
override def getById(id: Int)(
implicit ec: ExecutionContext): Future[\/[Throwable, Option[User]]] = {
val q = for {
u <- users if u.id === id
} yield u
dbComponent.db.run(q.result.headOption).map(_.right[Throwable]).recover {
case NonFatal(ex) => ex.left[Option[User]]
}
}
/** Add user
*
* @param user user
* @return created id
*/
override def add(user: User)(implicit ec: ExecutionContext): Future[\/[Throwable, Int]] =
dbComponent.db.run(users returning users.map(_.id) += user).map(_.right[Throwable]).recover {
case NonFatal(ex) => ex.left[Int]
}
/** Update user
*
* @param user user
* @return update count
*/
override def update(user: User)(implicit ec: ExecutionContext): Future[\/[Throwable, Int]] = {
val q = for {
u <- users if u.id === user.id
} yield u.name
dbComponent.db.run(q.update(user.name)).map(_.right[Throwable]).recover {
case NonFatal(ex) => ex.left[Int]
}
}
/** Delete user
*
* @param id identity
* @return delete count
*/
override def delete(id: Int)(implicit ec: ExecutionContext): Future[\/[Throwable, Int]] = {
val q = for {
u <- users if u.id === id
} yield u
dbComponent.db.run(q.delete).map(_.right[Throwable]).recover {
case NonFatal(ex) => ex.left[Int]
}
}
private class UserTable(tag: Tag) extends Table[User](tag, "user") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
override def * = (id.?, name) <> (User.tupled, User.unapply)
}
}
DBComponent
をインジェクションすることでドライバに依存せずに実装することができました。
こやつを使用するときにUserRepositoryImplDI(new H2DBComponent{})
とか、UserRepositoryImplDI(new MySQLDBComponent{})
とすれば各ドライバで動作させることが可能です(コンポーネントのnew宣言がキモかったらどっかでオブジェクトにでもすればいいかも・・・)。
終わりに
テストにH2を使って、本番にMySQLを使って書いたものを上げておきます(おまけで自分型バージョンの実装も載せてます)。
追記
上げてから関連記事欄で @mather314 さんのSlickでJDBCドライバを実行時に切り替えるメモ(ver. 2.x)という投稿にもろカブリしてるのに気がつきました。すみません!!
バージョン3系なので許してください!