動機
金曜日くらいにPlay-Slickに入門しようと、ドキュメントを漁りつつ一からいじっていたのですが、Slickがそもそもまったくわからないので(Playも当然まったくわかっていませんが…)、Slick3.1のチュートリアルをやろうと思い立ちました。
Getting started
ところで、activatorで入門用のプロジェクトを作ると、サンプルコードがすでに完成していて、チュートリアルをやりたいのにチュートリアルがないということがよくあります。
たとえば、Play-Scalaをはじめてやりたいであろう人用のactivatorのテンプレート、play-scala-intro
ですが、その紹介ページにこんなコメントが有ります。
Where is the tutorial?
(私訳) で、チュートリアルはどこにあるん?
これは、Hello Slick(Slick3.1)でも同じことが言えて、これも動くコードが完成していて、helloどころではないという残念な状況になっていました。
仕方ないので、Hello Scala 2.11!で新規プロジェクトを作って、Hello Slick(Slick3.1)のテンプレートのリポジトリーを眺めつつ、かつ、それを別のプロジェクトとして準備しておいて、そのコードを写経することにしました。
なお、写経していくコードは、activator-hello-slick
レポジトリーのslick-3.1
ブランチですので、お間違えなく。
テーブル
これは僕の悪い点なのですが、写経するコードがすごい簡単に見えてしまうため、どうしても自分でオリジナリティを加えてしまおうとすることです。
例えば、Hello-Slick(Slick3.1)の中にはSuppliers
とCoffees
というテーブルを表すクラスが出てきます。しかし、タプルで扱うのがいやなので、僕はここに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(_))
さて、この???
の部分ですが、サンプルコードのタプルの例ではどうするべきかわかりません。で、ドキュメントを漁ることになります。
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で質問することにしました。
で、教えてもらったのが、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完全マスターになりました
と、言いたいところなのですが…
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完全マスター(一歩手前)です。