Slick 3 でちょっと複雑なDB処理を組み立てる

  • 25
    Like
  • 0
    Comment

概要

Slick 3 になって、DB操作を非同期に行えるようになった。

Slick 2 までは同期的であったため、複数クエリが関わる操作も「なんとなく」書けていたが、非同期で flatMap (for - yield) を使って書いていると、ちょっとややこしくなってくる。

その理由は、 2つのモナドがある ということだけなのだが、この2つがごっちゃになっていると訳が分からなくなる。
また、簡単なクエリを組み立てるだけのコードはぐぐればいくらでも出てくるが、「ちょっと複雑な」DB処理はあまり出てこない。

そこで、この2つのモナドについて解説しつつ、「ちょっと複雑な」DB処理をいくつか紹介する。

対象読者

  • Scala の flatMap とか for - yield 文を使うことができる。
  • RDB の基礎は押さえている
  • Slick 3.1 で簡単な CRUD は書けるけど、複雑なのは難しい

サンプルコード

https://github.com/srd7/slick-3.1-example
ごく簡単なブログアプリです。

ブログアプリのDB構成

untitled.png

シンプルですね。普通ですね。
USERCOUNT って何?
そのユーザーが書いたブログの数のキャッシュです。
トランザクションの説明のために置いたものなので、石を投げないでください。

2つのモナド

Query

大まかに言うと、1つのテーブル操作 のためのモナドです。
Slickのソースコードはこちら

実際に書くのは

users // TableQuery は Query の子クラス
users.filter(_.id === 1) // ここで止めた場合!

とかですね。
API ドキュメントで確認しておきましょう。

これはモナドなので flatMap で組み合わせて

for {
  user <- users
  blog <- blogs if blog.id === user.id
} yield (user, blog)

などとすることができます。
なお上例の型を正確に書くと Query[(Users, Blogs), (User, Blog), Seq] になります。長いですね。
私は面倒なので、この型は省略するようにしてます。

まとめると、Query は、1つのSQL文を組み立てるための材料 です。

DBIO

上記Queryから結果を取得する時に

users.result
users.filter(_.id === 1).result.headOption

のように result やら result.headOption やらを書きます。

イメージ的には result を書くと SQL 文として固めるような感覚です。
実際にはそういうわけではないけど、あくまでもイメージとして。

これにより FixedSqlStreamingAction 型になります。
この型をさらにたどっていくと DBIOAction 型になります。

DBIOAction は型パラメータを3つ取り面倒な子で、Slick側が DBIO という alias を用意してくれています。

DBIOAction[Seq[Blog], Streaming[Blog], slick.dbio.Effect.Read] 型が DBIO[Seq[Blog]] として記述できます。

詳しいことは API ドキュメントを見てください。

いろいろ書きましたが、DBIO型になると思って間違いないです。

先ほどの例で具体的に示すと、

// DBIO[Seq[User]]
users.result

// DBIO[Option[User]]
users.filter(_.id === 1).result.headOption

// DBIO[Seq[(User, Blog)]]
( for {
  user <- users
  blog <- blogs if blog.id === user.id
} yield (user, blog) ).result

って感じです。

さてここまでは SELECT 文に相当するものしか出てきませんでした。

INSERT UPDATE DELETE に相当するもの

users += user
users.filter(_.id === user.id).update(user)
users.filter(_.id === user.id).delete

は、これだけで FixedSqlAction を返します。
「これだけで」と言うのは、まぁ当然なんですが、

(users += user).result
(users.filter(_.id === user.id).update(user)).result
(users.filter(_.id === user.id).delete).result

とかしてもエラーになりますよってことです。

さて、この FixedSqlAction なんですが、DBIOAction の派生クラスです。
結論だけ言うと、すべて DBIO[Int] 型です

まとめると、DBIO1つのSQL文でのDB操作を表します

実際の使い方

2つのモナドをもう一度まとめましょう。

Query: 1つのテーブルへの処理で、1つのSQL文の組み立てるのに使う
DBIO : 1つのSQL文の処理で、トランザクションを組み立てるのに使う

間違えちゃった例

どちらも flatMap(for - yield) で書くため、この2つがごっちゃになっていると混乱して

// 俺は1つのSQL文でDELETEしたいんだ!!
for {
  blog <- blogs.filter(_.id === blogId)
  _    <- users.filter(_.blogId === blog.id).delete
} yield ()

みたいなことをしてしまいます。

上の行は Query で、下の行は DBIO なのでおかしいですね。
動くようにするには

// implicit ExecutionContext が必要なので注意
// 2つのSQL文が発行されるので注意
// `head`を使っているため、blogId がなかったらエラーになるので注意
for {
  blog <- blogs.filter(_.id === blogId).result.head
  _    <- users.filter(_.blogId === blog.id).delete
} yield ()

とかできますが、コメントに書いた通りの注意点があります。

ちなみに、実際にこの DELETE 文を1つのSQLでやりたければ、生SQL書くべきだと思います。

正しいトランザクション

私のサンプルのここを参照してください。

コメント等を除いたコードをここにも貼っておきます。

def createBlog(blog: Blog)(implicit ec: ExecutionContext): DBIO[Unit] = {
  val action = for {
    userOp <- users.filter(_.id === blog.userId).result.headOption
    _      <- userOp.fold[DBIO[Int]](
      DBIO.successful(1)
    )(user =>
      blogs += blog
    )
    _      <- userOp.fold[DBIO[Int]](
      DBIO.successful(1)
    )(user =>
      users.filter(_.id === user.id).map(_.count).update(user.count + 1)
    )
  } yield ()

  action.transactionally
}

やっていることは

  1. user を探す
  2. blog を insert する
  3. user の counter を increment する

です。
1でユーザーが見つからなかった場合は、2と3をする必要ないですね。
前述の通り、CRUD全操作は DBIO型として扱えます。
foldで型明示しているのは、FixedSqlAction とか FixedSqlStreamingAction とかが絡むので「型が違うよ」と怒られるため、型制約を緩くするためです。

ちなみに transactionallyDBIO にはなく、暗黙変換により JdbcActionExtensionMethodsから呼ばれます。(ソースコード
そのため

import slick.driver.H2Driver.api._ // H2 Database の場合

をしておかないと怒られるので注意。

まとめ

QueryDBIO をしっかり意識して区別すれば大したことないです。

参考