LoginSignup
10
7

More than 5 years have passed since last update.

Slickでデータベースドライバを極力楽して切り替えたいんだけど・・・

Last updated at Posted at 2018-06-16

始めに

Slickの公式ドキュメントって基本使用するデータベース決め打ち前提(import slick.jdbc.H2Profile.api._等)で書かれてるけど、やっぱり使ってると本番環境はMySQLだけどテストはH2でさらっと済ましたいってことあるじゃないですか。

でもslick.jdbc.H2Profileにしてもslick.jdbc.MySQLProfileにしてもslick.jdbc.JdbcProfileを継承してるのは分かるけど、内部が複雑怪奇でどう手を付けていいのか分からん!!

それでも何とかならんのかって調べてみたところ、こんな記事に当たったので早速、パクらせて参考にさせてもらって書いてみます。

各Profileのコンポーネント化

上記の参考元では、JdbcProfileを抽象化したコンポーネント(trait)を継承したリポジトリやサービス等を組んでおいて、実装時に各DBのドライバで具象化したコンポーネント(trait)を引き渡してやればいいんだぜって感じで書いてあるんじゃないかと思います・・・たぶん。

とりあえず書けば分かる!!

まず、抽象的なコンポーネントを書いてみます。JdbcProfileを使用しますからって宣言的なtraitです。

DBComponent.scala

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ドライバ用の具象化コンポーネントを用意します。

H2DBComponent.scala
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)

}

MySQLDBComponent.scala
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というモデルを作りました。

Model.scala
package com.example.repository

/** Model **/
object Model {

  /** User
    *
    * @param id identity
    * @param name name
    */
  final case class User(id: Option[Int], name: String)

}

リポジトリを実装します。本当はスキーマ定義は切り出した方が良いとは思いますが、今回は手抜きで。
UserRepositoryを継承していますが、これは単なるインターフェースなので載せるのは省略します。

UserRepositoryImplDI.scala
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系なので許してください!

10
7
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
10
7