slick2とslick3を同居させる

slick2からslick3への移行はAPIが根本的に変わったためにめちゃくちゃ大変です。blocking-slick を使った方法なども知られていますが、ちょっと使って良いものかは悩みます。

blocking-slickを使っても使わなくてもslick2からslick3への書き換えを一度にやるのは大きなプロジェクトだとかなりのパワーが必要です。slick2とslick3を1つのプロジェクト内で同居させ、徐々にslick3に移行させていくことはできないでしょうか。

通常、同一ライブラリの2つのバージョンがプロジェクト内に同居するということはありえません。ところがslick2とslick3の場合、package名が

  • slick2: scala.slick.*
  • slick3: slick.*

と変わっているのでslick2とslick3の両方を依存関係に加えることができればできるはずです。

slick2とslick3の両方を依存関係に加える

単純に考えると次のようなことがしたいのですが、これはできません。

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "2.1.0",
  "com.typesafe.slick" %% "slick" % "3.2.1"
)

同一ライブラリの別バージョンがあった場合、sbtはバージョンが新しいものを優先して依存関係に追加します。そのためslick2は読み込んでくれません。sbtのバージョンによっては次のようなメッセージが出ているでしょう。

[warn] There may be incompatibilities among your library dependencies.
[warn] Here are some of the libraries that were evicted:
[warn]  * com.typesafe.slick:slick_2.11:2.1.0 -> 3.2.1

sbtに依存関係を管理させるとslick2とslick3を同居させることはできなそうです。幸い、sbtはlibというディレクトリにjarを追加することでそのjarを読み込んでくれますから、slick3のjarを自分で用意してlibに入れます。

バイナリ互換性について

slick2はtypesafe/configの1.2に依存しているけどslick3はtypesafe/configの1.3に依存している。でもtypesafe/configの1.2と1.3はバイナリ互換なようでセーフ。

コード例

というわけで次のコードはslick2とslick3を同居させた例です。

package com.example

import scala.concurrent.ExecutionContext.Implicits.global

object Slick3 {
  import _root_.slick.jdbc.H2Profile.api._

  val db = Database.forConfig("h2mem1")

  class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
    def id = column[Int]("SUP_ID", O.PrimaryKey) // This is the primary key column
    def name = column[String]("SUP_NAME")
    def street = column[String]("STREET")
    def city = column[String]("CITY")
    def state = column[String]("STATE")
    def zip = column[String]("ZIP")
    // Every table needs a * projection with the same type as the table's type parameter
    def * = (id, name, street, city, state, zip)
  }

  val suppliers = TableQuery[Suppliers]

  // Definition of the COFFEES table
  class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
    def name = column[String]("COF_NAME", O.PrimaryKey)
    def supID = column[Int]("SUP_ID")
    def price = column[Double]("PRICE")
    def sales = column[Int]("SALES")
    def total = column[Int]("TOTAL")
    def * = (name, supID, price, sales, total)
    // A reified foreign key relation that can be navigated to create a join
    def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
  }

  val coffees = TableQuery[Coffees]

  def setup() = {
    val action = DBIO.seq(
      // Create the tables, including primary and foreign keys
      (suppliers.schema ++ coffees.schema).create,

      // Insert some suppliers
      suppliers += (101, "Acme, Inc.",      "99 Market Street", "Groundsville", "CA", "95199"),
      suppliers += ( 49, "Superior Coffee", "1 Party Place",    "Mendocino",    "CA", "95460"),
      suppliers += (150, "The High Ground", "100 Coffee Lane",  "Meadows",      "CA", "93966"),
      // Equivalent SQL code:
      // insert into SUPPLIERS(SUP_ID, SUP_NAME, STREET, CITY, STATE, ZIP) values (?,?,?,?,?,?)

      // Insert some coffees (using JDBC's batch insert feature, if supported by the DB)
      coffees ++= Seq(
        ("Colombian",         101, 7.99, 0, 0),
        ("French_Roast",       49, 8.99, 0, 0),
        ("Espresso",          150, 9.99, 0, 0),
        ("Colombian_Decaf",   101, 8.99, 0, 0),
        ("French_Roast_Decaf", 49, 9.99, 0, 0)
      )
      // Equivalent SQL code:
      // insert into COFFEES(COF_NAME, SUP_ID, PRICE, SALES, TOTAL) values (?,?,?,?,?)
    )
    db.run(action)
  }

  def run(): Unit = {
    println("Coffees:")
    for {
      _ <- setup()
      result <- db.run(coffees.result)
    } {
      result.foreach {
        case (name, supID, price, sales, total) =>
          println("  " + name + "\t" + supID + "\t" + price + "\t" + sales + "\t" + total)
      }
    }
  }
}

object Slick2 {
  import scala.slick.driver.H2Driver.simple._

  val db = Database.forConfig("h2mem2")

  class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
    def id = column[Int]("SUP_ID", O.PrimaryKey) // This is the primary key column
    def name = column[String]("SUP_NAME")
    def street = column[String]("STREET")
    def city = column[String]("CITY")
    def state = column[String]("STATE")
    def zip = column[String]("ZIP")
    // Every table needs a * projection with the same type as the table's type parameter
    def * = (id, name, street, city, state, zip)
  }

  val suppliers = TableQuery[Suppliers]

  // Definition of the COFFEES table
  class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
    def name = column[String]("COF_NAME", O.PrimaryKey)
    def supID = column[Int]("SUP_ID")
    def price = column[Double]("PRICE")
    def sales = column[Int]("SALES")
    def total = column[Int]("TOTAL")
    def * = (name, supID, price, sales, total)
    // A reified foreign key relation that can be navigated to create a join
    def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
  }

  val coffees = TableQuery[Coffees]

  def setup() = {
    db.withSession { implicit session =>
      // Create the tables, including primary and foreign keys
      suppliers.ddl.create
      coffees.ddl.create

      // Insert some suppliers
      suppliers += (101, "Acme, Inc.",      "99 Market Street", "Groundsville", "CA", "95199")
      suppliers += ( 49, "Superior Coffee", "1 Party Place",    "Mendocino",    "CA", "95460")
      suppliers += (150, "The High Ground", "100 Coffee Lane",  "Meadows",      "CA", "93966")
      // Equivalent SQL code:
      // insert into SUPPLIERS(SUP_ID, SUP_NAME, STREET, CITY, STATE, ZIP) values (?,?,?,?,?,?)

      // Insert some coffees (using JDBC's batch insert feature, if supported by the DB)
      coffees ++= Seq(
        ("Colombian",         101, 7.99, 0, 0),
        ("French_Roast",       49, 8.99, 0, 0),
        ("Espresso",          150, 9.99, 0, 0),
        ("Colombian_Decaf",   101, 8.99, 0, 0),
        ("French_Roast_Decaf", 49, 9.99, 0, 0)
      )
      // Equivalent SQL code:
      // insert into COFFEES(COF_NAME, SUP_ID, PRICE, SALES, TOTAL) values (?,?,?,?,?)
    }
  }

  def run(): Unit = {
    println("Coffees:")
    db.withSession { implicit session =>
      coffees.list.foreach {
        case (name, supID, price, sales, total) =>
          println("  " + name + "\t" + supID + "\t" + price + "\t" + sales + "\t" + total)
      }
    }
  }
}

object Main {

  def main(args: Array[String]): Unit = {
    println("------------- Slick3 --------------")
    Slick3.setup()
    Slick3.run()
    println("------------- Slick2 --------------")
    Slick2.setup()
    Slick2.run()
  }

}

slick3のクラスをimportする時は _root_ を頭につけた方が良いと思います。scala

import scala.io.Source

import io.Source

と書けるように、scalaパッケージがデフォルトでimportされているので、_root_ をつけないとslick3を読み込みたいのかslick2を読み込みたいのか不明瞭になってしまうためです。

というわけでslick2とslick3を同居させることができたんですが、これはこれで新たな地獄を生み出してしまったのかもしれません。