Slick3のSQLクエリを複数結合する

Slickにて、クエリを連続的に結合する方法についてのまとめ
忘備録的なものなので、用語に関してなど、間違っているところがあるかもしれないです。
ツッコミしていただけたらありがたいです。

環境

  • play2.4
  • Slick3.0
  • mysql


Slickでクエリを発行する際に、同一のWeb Requestの中でdb.runを何回も発行するとトランザクションが別になってしまうので、片方のトランザクションだけが失敗してしまって原子性が保たれないなどの場合がある。
同一トランザクションにするためにはすべてのクエリ(DBIOAction)を一つにまとめる必要がある。


データベースに対する操作はDBIOActionと呼ばれる

  • accountTable.schema.create
  • accountTable.map(_.userId === 1).result
  • accountTable += Account(1,”name”,”email)
  • (users returning users.map(_.id)) += User(None, “Stefan”, “Zeiger”)


これらはすべてDBIOActionであり、db.run( DBIOAction ) と実行できる類のものの事になります。

これらを同一のトランザクションとして処理したい場合、単純な方法としてはマニュアルに乗っているようにすればよい。
DBIO.seqの中に全部放り込めば良いので、上記のDBIOActionを例にすると以下のようになる。

Slick3.0.3 Manual
http://slick.typesafe.com/doc/3.0.0/gettingstarted.html#populating-the-database

val setup = DBIO.seq(
  	accountTable.schema.create,
	accountTable.map(_.id == 1).result,
	accountTable += Account(1,"name","email),
	(accountTable returning accountTable .map(_.id)) += Account(1,"name","email)
)
val setupFuture = db.run(setup.transactionally)

これですべてのクエリが実行されるわけだが、クエリの結果は取得することができない。ひとつとして。

さらに実際はSelectした結果をInsertしたい場合もあるだろうし、AutoincrementされたIdを元に他のレコードをUpdateしたい場合など、
クエリの結果を取得したうえで次のクエリを行うには次のように考えれば良い。


参考にしたいのがマニュアルにあるこちらのサンプル
for yield 文を使ってうまくクエリの結合をしている。

Slick3.0.3 Manual
http://slick.typesafe.com/doc/3.0.0/dbio.html#transactions-and-pinned-sessions

val a = (for {
  ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
  _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

val f: Future[Unit] = db.run(a)

もう少し単純でわかりやすい形のサンプルにしてみる(これは実際に役に立つ例ではないが)

val coffees = TableQuery[CoffeeTable]
val a = (
  for {
    gotIds <- coffees.filter(_.type === "ESPRESSO").map(_.id).result
    gotCoffees <- coffees.filter(_.id === gotId.head).result
} yield (gotCoffees))

val f: Future[Seq[Coffee]] = db.run(a.transactionally)

このサンプルではまず3行目でtypeが“ESPRESSO”のレコードのIDをすべて取得する。そして取得されたすべてのidが左に示したgotIdsにSeqとして代入される。
次4行目でgotIdsと一致するレコードをCoffeeテーブルから取得する。それが左に示したgotCoffeesに代入される。(ここではheadを使い取得したgotIdの最初の1つと一致するものを取得している)
そして最後 yield に指定されたgotCoffeesが最終的にFutureから取得できる結果となり、Seq[Coffee]である。
もしyield(godIds)とすれば最後に取得できる値はSeq[Int] になる。
両方欲しければ yield((gotCoffese, gotIds)) とすれば両方の値が(Seq[Coffee], Seq[Int])とタプルで取得できるようになる。


まとめると
forの中の各行にクエリを記述し、取得される値を左辺の変数に代入、
次の行では同様にクエリを記述でき、前の行で取得した変数のリストを利用することもできる。
これをいくつも繰り返すことができるような形になっている。
そして、取得したい結果を左辺の変数より、yield に記述する。

そして最後にdb.runするまえにtransactionallyを指定しておけばコミット処理やロールバックを自動で行ってくれるため、原子性が守られることになる。


もう一つ少し実用的な応用のサンプルも一つ挙げてきます。
ESPRESSOの種類のCoffeのIDを取得し、時間とともにオーダーに加えるというサンプルです。
取得したIDを元にOrderインスタンスを作成しinsertするようなことも一つのトランザクションで可能です。

val coffees = TableQuery[CoffeeTable]
val orders= TableQuery[OrdersTable]
val a = (
  for {
    gotIds <- coffees.filter(_.type=== "ESPRESSO").map(_.id).result
    _ <- orders += Order(gotIds.head, "order time")
} yield ())

val f: Future[Seq[Unit]] = db.run(a.transactionally)

ところで、なぜこれらのクエリーがfor yield文で記述されるのか気になった方もいると思います。
for yieldはscalaではflatMap関数に変換されて処理されるところがポイントになります。

このfor yield文をflatMap関数で表していくと次のように表すことができます。

coffees.filter(_.type=== "ESPRESSO").map(_.id).result.flatMap( gotIds:Seq[Int] =>
    orders += Order(gotIds.head, "order time").map( _ => ... )

いままでのクエリがどのように組み立てられているか、なんとなくわかったのではないでしょうか。


DBIOAction.flatMap(result1:Any =>
  DBIOAction.flatMap(result2:Any => 
    DBIOAction.flatMap(result3:Any =>
      DBIOAction.flatMap(result4:Any => 

こんなかんじでクエリが作られていくようです。
この辺の仕組みがわかると、ようやくSlick3と仲良くなれたような気がしました。


トラックバックURL  -  http://mashi.exciton.jp/archives/189/trackback