mike-neckのブログ

Java or Groovy or Swift or Golang

世間から4周遅れでSlick3.1完全マスター(一歩手前)した #scala

動機

金曜日くらいにPlay-Slickに入門しようと、ドキュメントを漁りつつ一からいじっていたのですが、Slickがそもそもまったくわからないので(Playも当然まったくわかっていませんが…)、Slick3.1のチュートリアルをやろうと思い立ちました。

Getting started

ところで、activatorで入門用のプロジェクトを作ると、サンプルコードがすでに完成していて、チュートリアルをやりたいのにチュートリアルがないということがよくあります。

たとえば、Play-Scalaをはじめてやりたいであろう人用のactivatorのテンプレート、play-scala-introですが、その紹介ページにこんなコメントが有ります。

www.typesafe.com

Where is the tutorial?

(私訳) で、チュートリアルはどこにあるん?

これは、Hello Slick(Slick3.1)でも同じことが言えて、これも動くコードが完成していて、helloどころではないという残念な状況になっていました。

www.typesafe.com

仕方ないので、Hello Scala 2.11!で新規プロジェクトを作って、Hello Slick(Slick3.1)のテンプレートのリポジトリーを眺めつつ、かつ、それを別のプロジェクトとして準備しておいて、そのコードを写経することにしました。

www.typesafe.com

github.com

なお、写経していくコードは、activator-hello-slickレポジトリーslick-3.1ブランチですので、お間違えなく。


テーブル

これは僕の悪い点なのですが、写経するコードがすごい簡単に見えてしまうため、どうしても自分でオリジナリティを加えてしまおうとすることです。

例えば、Hello-Slick(Slick3.1)の中にはSuppliersCoffeesというテーブルを表すクラスが出てきます。しかし、タプルで扱うのがいやなので、僕はここにcase classを使ってみることにしました。

(ここでの例ではCoffeesのみ)

case class Coffee(
    id: Option[Long],
    name: String,
    supplier: Long,
    price: Double,
    sales: Int,
    total: Int
)

class Coffees(tag: Tag)
  extends Table[Coffee] (tag, "COFFEE") {

  def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def name: Rep[String] = column[String]("NAME")
  def supplier: Rep[Long] = column[Long]("SUPPLIER")
  def price: Rep[Double] = column[Double]("PRICE")
  def sales: Rep[Int] = column[Int]("SALES")
  def total: Rep[Int] = column[Int]("TOTAL")
  
  override def * : ProvenShape[Coffee] = ???

  def supplierRef: ForeignKeyQuery[Suppliers, Supplier] =
    foreignKey(
      "SUPPLIER_FK",
      supplier,
      TableQuery[Suppliers])(_.id)
}

object Coffees extends TableQuery(new Coffees(_))

さて、この???の部分ですが、サンプルコードのタプルの例ではどうするべきかわかりません。で、ドキュメントを漁ることになります。

Schemas - Mapped Table

15分くらいかけてやっと、ここにたどり着きました。

上記のテーブルの場合は次のようにやるのが正しいようです。

  override def * : ProvenShape[Coffee] =
    (id.?, name, supplier, price, sales, total) <> (Coffee.tupled, Coffee.unapply)

また、自前でcase classにコンパニオンオブジェクトを作っていたりすると、Coffee.tupledというメソッドが生えないことがあるらしいですが、ドキュメントに注意書きがありました。

For case classes with hand-written companion objects, .tupled only works if you manually extend the correct Scala function type. Alternatively you can use (User.apply _).tupled.

(意訳) 自分でcase classのコンパニオンオブジェクトを書いた場合には、自分でtupledを定義してください。あるいは(User.apply _).tupledで代用できます。

本当に初めてやる場合はサンプルコードを真似したほうがよい

これも僕の悪い点なのですが、Scalaでもちゃんと型を明示しておきたいというのがあります。さらに、悪い点ですが、出来る限りimportには_を使いたくなかったりします。

その結果、まずテーブルの生成でつまずきます。

import slick.driver.H2Driver.api.{DBIO}

object HelloSlick extends App {
  val db: Database = Database.forConfig("h2tcp")
  try {
    val setup: DBIO[Unit] =
      (Suppliers.schema ++ Coffees.schema).create
  }
}

と書いたところで、schemaが解決できないとコンパイルエラーになります。

ここでTableQueryに何かしら別のクラスのschemaというメソッドが扱えるようなimplicitなんたらが適用されているのだろうと想像できます。

そこで、Hello-Slick(Slick3.1)を別のプロジェクトとして作っておいたことが役立ちます。このプロジェクトをIntelliJ IDEAにインポートして、コンパイルが通っている場所で、schemaメソッドの定義を見に行きます。すると、schemaメソッドslick.profile.RelationalProfile.TableQueryExtensionMethodsというクラスで定義されていることがわかりました。で、TableQueryExtensionMethodsが使われているところを探すと、slick.profile.RelationalProfile.APIトレイトの中にimplicit def tableQueryToTableQueryExtensionMethodsにて使われていることがわかります。あとは、slick.driver.H2Driver.apiから辿って行くと、ここのslick.profile.RelationalProfile.APIにたどり着けるので、これをimportの対象にすればよいのかということがわかりました。

で、変更したあとのコードは次のようになります。

import slick.driver.H2Driver.api.{DBIO, tableQueryToTableQueryExtensionMethods}

object HelloSlick extends App {
  val db: Database = Database.forConfig("h2tcp")
  try {
    val setup: DBIO[Unit] =
      (Suppliers.schema ++ Coffees.schema).create
  }
}

すると、次はcreateというメソッドが解決できないと…

一事が万事、この調子ですから、普通に初心者はimport slick.driver.H2Driver.api._としてインポートしたほうが速いです。

ところで、僕のほうは信念を曲げるのが無駄に嫌だったので、先ほどのような調子でやり続けていたのですが、どうしても解決できないimplicitなんたらがありました。

具体的には

  val cofeeNames: StreamingDBIO[Seq[String], String] = Coffees.map(_.name).result

mapの引数の型にslick.lifted.FlatShapeLevelが求められるのに対して、実際の型はslick.lifted.Rep[String]だから型が一致しないというものです。

40分くらい頭を捻ってましたが諦めて、空気を読まずにscalajp/publicのgitterで質問することにしました。

gitter.im

で、教えてもらったのが、scalacのコンパイルオプションに-Xprint:typerをつけるという方法です。

そこで、Hello-Slick(Slick3.1)の方のbuild.sbtに次のようにscalacオプションを追加します。

scalacOptions ++= Seq("-Xprint:typer")

これを付与すると、コンパイルされた後に、どのimplicitなんとかが使われているかがわかるようになります。

  val coffeeNamesAction: slick.driver.H2Driver.api.StreamingDBIO[Seq[String],String] = slick.driver.H2Driver.api.streamableQueryActionExtensionMethods[String, Seq](coffees.map[slick.driver.H2Driver.api.Rep[String], slick.lifted.Rep[String], String](((x$3: Coffees) => x$3.name))(lifted.this.Shape.repColumnShape[String, Nothing](slick.driver.H2Driver.api.stringColumnType))).result;

ここから、slick.driver.H2Driver.api.stringColumnTypeがインポートされていないので、コンパイルエラーになるということがわかりました。

というわけで、本当に時間がかかるので、初めてやる場合は、サンプルコードにあるとおりにimport slick.driver.H2Driver.api._でインポートしたほうが圧倒的に楽です。


成果物

というわけで、世間から4周遅れでSlick3.1完全マスターになりました

github.com

と、言いたいところなのですが…

JOINがうまくいかない

どうやっても、COFFEEテーブルとSUPPLIERテーブルのJOINができないんですね…

うまくいかないコード

val joinQuery: Query[(Rep[String], Rep[String], Rep[Double]), (String, String, Double), Seq] =
  for {
    c <- Coffees if c.price >= 340.0
    s <- Suppliers
  } yield (s.name, c.name, c.price)

これをどうやっても、直積しか得られないんですね。

生成されているSQLを見てみると、こんな感じ。

select
  x2."NAME",
  x3."NAME",
  x3."PRICE"
from
  "COFFEE" x3,
  "SUPPLIER" x2
where
  (x3."PRICE" >= 340.0) and
  true

(読みやすくするために改行してあります)

CoffeesクラスにつけたForeignKeyQuery[Suppliers, Supplier]をどうすれば直せるのか、Hello-Slick(Slick3.1)のコードなども調べてみましたが、解決策も見いだせず、仕方ないので、joinするクエリーを次のように書くことにしました。

val joinQuery: Query[(Rep[String], Rep[String], Rep[Double]), (String, String, Double), Seq] =
  for {
    (c, s) <- Coffees join Suppliers on (_.supplier === _.id)
    if c.price >= 340.0
  } yield (s.name, c.name, c.price)

ので、Hello-Slick(Slick3.1)にあるコードを全部ちゃんと動かせなかったのでSlick3.1完全マスター(一歩手前)です。