PlayのCache APIでMemcachedやRedisの代わりにRDBを使う

Play frameworkはキャッシュのためのAPIplay.api.cahe というパッケージで提供しています。play.api.cache はそのバックエンドとして、CaffineやEhcacheが選択でき、またサードバーティのライブラリを使えばMemcached, Redisなどをバックエンドにすることができます。キャッシュと言うと高速化のためのものですが、Playのキャッシュは高速化の他に、単なるデータの一時保存のため、つまり単なるKey-Value Storeとして使われていたりもします。

Caffeineを使うとインメモリのキャッシュが簡単に実現できますが、アプリケーションサーバーのインスタンスが複数ある場合、各サーバーでキャッシュした内容を共有することができません。MemcachedやRedisを使えばインスタンス間でキャッシュした内容を共有できますが、MemcachedやRedisを管理する手間が発生します。

PlayのキャッシュAPIは使いたいけれど、MemcachedやRedisなどの新たなミドルウェアを導入したくない。という状況で、ならばRDBをバックエンドにしてしまおうと作ったのがdbcacheというライブラリです。実際のところ、MemcachedやRedisを使わなくてもRDBで中小規模のサービスでは事足りてしまいます。

https://github.com/tototoshi/dbcache

設定

テーブル追加

MySQLをバックエンドとして使う場合の例です。まずデータベースにcache_entriesテーブルを作成します。

CREATE TABLE `cache_entries` (
  `cache_key` varchar(191) PRIMARY KEY,
  `cache_value` mediumblob NOT NULL,
  `expired_at` datetime,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  INDEX (`expired_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Play2.7.xの場合

Play 2.7.xを使っている場合は dbcache-mysqldbcache-play をbuild.sbtで追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.3.0",
  "com.github.tototoshi" %% "dbcache-play" % "0.3.0"
)

Play2.6.xの場合

dbcache-play は最近追加したモジュールなので、Play 2.6.xには対応していません。Play2.6.xを使っている場合は dbcache-mysql だけを追加し、以下のコードをアプリケーションコードに追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.2.0"
)
// Create adapter for play.api.cache
class DBCacheApi(myCache: DBCache) extends CacheApi {
  def set(key: String, value: Any, expiration: Duration): Unit =
    myCache.set(key, value, expiration)
  def get[A](key: String)(implicit ct: ClassTag[A]): Option[A] =
    myCache.get[A](key)
  def getOrElse[A: ClassTag](key: String, expiration: Duration)(orElse: => A) =
    myCache.getOrElse(key, expiration)(orElse)
  def remove(key: String) = myCache.remove(key)
}

DIの設定

あとはProviderを作ってDIしてあげるだけです。Scalikejdbcを使っている場合はこんな感じになります。

import java.sql.Connection

import com.github.tototoshi.dbcache.ConnectionFactory
import com.github.tototoshi.dbcache.mysql.MySQLCache
import com.github.tototoshi.dbcache.play.DBCacheApi
import javax.inject.{Inject, Provider}
import play.api.Environment
import play.api.cache.SyncCacheApi
import scalikejdbc.DB

class MySQLCacheApiProvider @Inject()(environment: Environment) extends Provider[SyncCacheApi] {

  private val connectionFactory = new ConnectionFactory {
    override def get(): Connection = {
      DB.autoCommitSession().connection
    }
  }
  override def get(): SyncCacheApi = {
    val mysqlCache = new MySQLCache(connectionFactory, environment.classLoader)
    new DBCacheApi(mysqlCache)
  }
}

EhCacheモジュールは無効にしておきます。

play.modules.disabled += "play.api.cache.ehcache.EhCacheModule"

あとは普通にPlayのキャッシュAPIを使えばデータベースに保存されるようになります。

運用について

期限切れのキャッシュデータがDBに残るのでバッチか何か動かしてたまに掃除してあげてください。

DELETE FROM cache_entries where expired_at < ${n日前}

いつもJavaConvertersでうまくいくとは限らない

ScalaJavaのコレクションを相互に変換するには scala.collection.JavaConverters が便利です。例えばJavaのコレクションに .asScala をつければ、JavaのコレクションをラップしたScalaのコレクションが作られます。ただし、いつも .asScala で済むとは限りません。コレクションのインターフェイスではなくデータ構造が重要な場合は注意が必要です。

次のコード例は java.util.LinkedHashMap.asScala に変換して、 +=, + の操作を行ったものです。 LinkedHashMap は要素が挿入された順番を保持するはずですが、 += を使った場合は順番が保持されるものの、 + を使った場合は順番が保持されていません。

val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

m2 += "field3" -> 3
m2 += "field4" -> 4
println(m2) // Map(field1 -> 1, field2 -> 2, field3 -> 3, field4 -> 4)
val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

val m3 = m2 + ("field3" -> 3) + ("field4" -> 4)
println(m3) // Map(field1 -> 1, field3 -> 3, field2 -> 2, field4 -> 4)

+= を使った場合、新しい要素は元のLinkedHashMap に挿入されます。一方、 + を使った場合は新たな Mapインスタンスが作られます。その新たな MapインスタンスJavaConverters で定義してあるラッパークラスが作るのですが、それが LinkedHashMap ではなく順番を保持しない HashMap なのです。

これは実際にplay-json 2.6.11に入り込んだバグです。play-jsonJsonのフィールドを Map として保持していますが、その MapScalaのものから java.util.LinkedHashMapインスタンスとして生成して、 JavaConverters でラップするという実装に変えたところ、Jsonのフィールドの順序が狂うという現象が起きました。

https://github.com/playframework/play-json/issues/236

というわけで、気をつけましょう。

例外をcase classとして定義してはいけない

Scalaで独自の例外を定義する場合

class MyException(message: String) extends RuntimeException(message)

case class MyException(message: String) extends RuntimeException(message)

のどちらが良いでしょうか。タイトルで言っちゃってますが、まずほとんどの場合はcase classではなくclassを使う方が良いと思います。

case classにすると等価性がそのインスタンスの属性によって判断されるので、同じ属性を持っている例外は同じものと見なされます。

scala> case class MyException(message: String) extends RuntimeException(message)
defined class MyException

scala> MyException("error")
res0: MyException = MyException: error

scala> MyException("error")
res1: MyException = MyException: error

scala> res0 == res1
res2: Boolean = true

一方、classの場合はequalsをオーバーライドしていない限り、属性が同じでもインスタンスが別なら別物です。

scala> class MyException(message: String) extends RuntimeException(message)
defined class MyException

scala> new MyException("error")
res0: MyException = MyException: error

scala> new MyException("error")
res1: MyException = MyException: error

scala> res0 == res1
res2: Boolean = false

基本的にはcase classではなくclassを使った時の挙動が好ましいと思います。messageが同じとはいえ、起きたタイミングの異なる2つの例外を同じものと見なすのは不自然ではないでしょうか。スタックトレースだって異なるかもしれないですしね。

AirframeでPlayを動かそうとした

taroleoさんが開発しているAirframeが最近Twitterのタイムラインで話題になっていることが多いので、そろそろ触ってみようかと思い、とりあえずPlay frameworkのDIコンテナをGuiceからAirframeに置き換えるというのをやって見ました。結果は失敗に終わったんですが、GuiceとAirframeの違いを知ることはできました。

PlayではデフォルトではRuntime DIにはGuiceが利用されますが、Play2.6ではDIコンテナのためのインターフェイスが作られたのでGuice以外のDIコンテナも利用できます。ApplicationLoaderというクラスがPlayのApplicationオブジェクトをロードする役割を担っているのでこれを好きなDIコンテナで実装します。

class AirframeApplicationLoader extends ApplicationLoader {
  override def load(context: ApplicationLoader.Context): Application = {
    import wvlet.airframe._
    // ここでAirframeでApplicationオブジェクトを構築する
  }
}

実際にはPlayとの橋渡し的なコードをそれなりに書く必要があり、そんなに簡単ではないです。例えばPlayが定義しているInjectorインターフェイスを実装する必要があったり、Playのビルトインモジュールを利用したいDIコンテナで使えるように変換するようなコードを書いたりです。実用的なことを考えるとPlayが提供しているGuiceApplicationBuilderみたいなコードも書く必要があり、なかなか骨が折れます。

Airframeの場合、実際にPlayの各モジュールをロードする段階でもつまづきます。Playの各モジュールは @Inject アノテーションでDIコンテナに対してクラスの生成方法のヒントを与えていますが、Airframeは @Inject アノテーションには対応していない方針のようでそれが機能しません。自分でクラスの生成方法を記述する必要があるのですが、Playの各モジュールの構築を手動でやるのはかなり面倒、ということで挫折しました。

その他Guiceとの違いで重要そうなのはsessionという概念でしょうか。Airframeではsessionが有効な間だけ機能を利用でき、また@PostConstruct, @PreDestroy アノテーションを利用するとsessionの開始時、終了時をフックして処理を挟みこめます。例えばスレッドの開始や終了などをAirframeに任せると便利ですね。Guiceにはこの機能はないので、Playでは @PostConstruct@PreDestroy の代わりに各クラスのコンストラクタとApplicationLifecycleというクラスでライフサイクルフックが実装されています。

同じDIコンテナという種類のライブラリでも比較して見るとそれなりに差異があります。それにDIコンテナはアプリケーションの基盤部分を担うようなものなので、気軽に置き換えられるようなものではないですね。でも気が向いたら再チャレンジしてみようと思います(多分やらない)。

play-jsonでReads/Writes/Formatを定義しなくてもよくする

play-jsonはReads/Writes/Formatを定義するのが面倒とよく言われるので、shapelessの練習も兼ねて作ってみました。

https://github.com/tototoshi/play-json-generic

scala> import play.api.libs.json._
import play.api.libs.json._

scala> case class Person(firstName: String, lastName: String, friends: List[Person])
defined class Person

scala> val people = Person("John",
     |                     "Lennon",
     |                     List(Person("Paul", "McCartney", Nil),
     |                          Person("George", "Harrison", Nil),
     |                          Person("Ringo", "Starr", Nil)))
people: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List())))

scala> Json.prettyPrint(Json.toJson(people))
<console>:18: error: No Json serializer found for type Person. Try to implement an implicit Writes or Format for this type.
       Json.prettyPrint(Json.toJson(people))
                                   ^

Reads/Writes/Formatを定義していないcase classをシリアライズしようとすると当然こんなエラーが出ますが、

import com.github.tototoshi.play.json.generic._ することでシリアライズ/デシリアライズできるようになります。

scala> import com.github.tototoshi.play.json.generic._
import com.github.tototoshi.play.json.generic._

scala> Json.prettyPrint(Json.toJson(people))
res1: String =
{
  "firstName" : "John",
  "lastName" : "Lennon",
  "friends" : [ {
    "firstName" : "Paul",
    "lastName" : "McCartney",
    "friends" : [ ]
  }, {
    "firstName" : "George",
    "lastName" : "Harrison",
    "friends" : [ ]
  }, {
    "firstName" : "Ringo",
    "lastName" : "Starr",
    "friends" : [ ]
  } ]
}

scala> Json.parse(res1).as[Person]
res2: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List())))

play-jsonJson.formatとかはマクロで実装されているので、そのマクロのためのオプションとかには一部対応できていないです。JsonNamingには対応できました。

Shapeless使ってみたいという人には The Type Astronaut's Guide to Shapeless - Underscore がすごくわかりやすくてオススメです。

追記

指摘されて気づいたんですが、HListを使っているので23個以上のフィールドを持つcase classにまつわる問題が解決しちゃってました。

https://github.com/tototoshi/play-json-generic/commit/0382c149efb4ed9fd4b4bcea99841641f2ca7949

Akka Streamをいい感じに停止させる

Akka Streamを使ったサーバーを書いていて、サーバーの終了処理を行う時にstreamを停止させる必要が出てきました。どうしたら良いでしょうか。

いきなり ActorSystem#terminate

とりあえずAkka Stream自体の終了のさせ方。これは無理やり ActorSystem#terminate を呼んでも止まることは止まります。

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

val src: Source[Int, NotUsed] = 
  Source.fromIterator[Int](() => Iterator.continually(1))
val sink: Sink[Int, Future[Int]] =
  Sink.fold(0)(_ + _)
val result = src.runWith(sink)

Thread.sleep(1000)

system.terminate()

logger.info("result:" + Await.result(result, Duration.Inf)))

ですが、以下のようなエラーが出てしまいます。ダメそうです。

Exception in thread "main" akka.stream.AbruptStageTerminationException: GraphStage [akka.stream.impl.HeadOptionStage$$anon$3@783971c3] terminated abruptly, caused by for example materializer or actor system termination.

KillSwitch

というわけで適当にググるhttps://stackoverflow.com/a/38326082 というStackOverflowの回答がすぐに出てきます。どうやらKillSwitchというのを使えば良いようです。(Konrad さんの回答なので安心)ちなみにちゃんとAkkaのドキュメントにも載っています。

こんな感じ

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1))
val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _)

val (killSwitch, result) = src
  .viaMat(KillSwitches.single)(Keep.right)
  .toMat(sink)(Keep.both)
  .run()

Thread.sleep(1000)

killSwitch.shutdown()

system.terminate()

logger.info("result: " + Await.result(result, Duration.Inf))      

KillSwitchでまずストリームを終了させ、それからActorSystemを終了させればエラーは起きません。

JVMのshutdown hookを使う

さて、サーバーの終了時にAkka Streamを停止させたいので Runtime.getRuntime.addShutdownHook を使うことにしました。

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1))
val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _)

val (killSwitch, result) = src
  .viaMat(KillSwitches.single)(Keep.right)
  .toMat(sink)(Keep.both)
  .run()

Runtime.getRuntime.addShutdownHook(new Thread(() => {
  killSwitch.shutdown()
  logger.info("result: " + Await.result(result, Duration.Inf))
  system.terminate()
}))

一見良さそうですが、これはエラーになることがあります。

Exception in thread "Thread-1" akka.stream.AbruptStageTerminationException: GraphStage [akka.stream.impl.HeadOptionStage$$anon$3@773caeb2] terminated abruptly, caused by for example materializer or actor system termination.

なぜ?

CoordinatedShutdown

不思議に思ってActorSystemのコードを読んでいたら、ActorSystemのインスタンスが作られる時には、JVMのshutdown hookにActorSystemの終了処理が追加されていることを知りました。これが原因でActorSystemに依存するような処理、例えばAkka StreamをJVMのshutdown hookで行おうとしても、タイミングによっては先にActorSystemが終了しうまくいかないようです。

ActorSystemのshutdown hookはCoordinatedShutdownというクラスで登録されています。そしてActorSystemに依存する処理のShutdownもCoordinatedShutdownを使うことで安全に行えます。次のコードがその例で、ActorSystemが終了する前(CoordinatedShtudown.PhaseBeforeActorSystemTerminateという値で指定している)にAkka Streamの終了処理を行っています。

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1))
val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _)

val (killSwitch, result) = src
  .viaMat(KillSwitches.single)(Keep.right)
  .toMat(sink)(Keep.both)
  .run()

CoordinatedShutdown(system).addTask(
  CoordinatedShutdown.PhaseBeforeActorSystemTerminate, "app-shutdown") {
    () =>
      killSwitch.shutdown()
      logger.info("result: " + Await.result(result, Duration.Inf))
      // ActorSystemはCoordinatedShutdownを使っていれば勝手に終了する
      // system.terminate()
      Future.successful(akka.Done)
}

ちなみにCoordinatedShutdownによる終了処理はJVMのshutdown hookに任せずに自分で呼ぶことも可能です。

CoordinatedShutdown(actorSystem)
  .run(CoordinatedShutdown.JvmExitReason)

CoordinatedShutdownは主にakka-clusterのための機能のようで、不要であれば使わないように設定することもできます。

https://doc.akka.io/docs/akka/2.5.11/project/migration-guide-2.4.x-2.5.x.html?language=scala#coordinated-shutdown

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を同居させることができたんですが、これはこれで新たな地獄を生み出してしまったのかもしれません。