Slick 2.0.0 documentation - 07 Queries

Permalink to Queries — Slick 2.0.0 documentation

Queries 

ここでは、Lifted Embedding APIを用いたデータの選択、挿入、更新、削除について、どのようにして型安全なクエリを書くか、ということについて説明を行う

Expressions 

(レコードでもコレクションでもない)スカラー値は、TypedType[T]が必ず存在しているという条件の元、(Rep[T]のサブタイプである)Column[T]によって表される。内部的な利用のために、Columnクラスにおいて、いくつかの特別な関数が直接定義されている。

それらのオペレータやlifted embeddingにおいて一般的に用いられる他の関数は、ExtensionMethodConversionsにおいて定義された暗黙的な変換を通して追加されている。実際に用いる関数についてはAnyExtensionMethodsColumnExtensionMethodsNumericColumnExtensionMethodsBooleanColumnExtensionMethodsStringColumnExtensionMethodsといったクラスにおいて定義がなされている(参照: ExtensionMethods)。

コレクション値はQuery(Rep[Seq[T]])クラスによって表される。これは、flatMapfiltertakegroupByのような多くの標準的なコレクション関数を持っている。2つの異なるQueryの複合型により、これらの関数のシグネチャは非常に複雑なものになっているが、本質的にはScalaのコレクションと同様の意味合いを持つ。

他にも、スカラー値のクエリに対しいくつかの関数がSingleColumnQueryExtensionMethodsへの暗黙的な変換を通して存在する。

Sorting and Filtering 

ソートやフィルタリングのための関数がいくつか用意されている(Queryを取り、新しい同じ型のQueryを返す)。例として、以下のようなものがある。

val q1 = coffees.filter(_.supID === 101)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     where "SUP_ID" = 101
...
val q2 = coffees.drop(10).take(5)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     limit 5 offset 10
...
val q3 = coffees.sortBy(_.name.desc.nullsFirst)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     order by "COF_NAME" desc nulls first

Joining and Zipping 

結合(join)は2つの異なるテーブルを結合し、何らかのクエリ処理を1つのクエリで実行するために用いられる。

結合を行うには2つの方法がある。明示的な結合では、2つのクエリを1つのクエリへと結合させる関数(innerJoinなど)を呼び出すことにより処理を実行させる。暗黙的な結合では、そのような関数を呼び出す事はせず、特有の記述を行うことで結合を行わさせる。

暗黙的な交差結合(cross join)Queryに対しflatMap操作を行うことで実行させる事が出来る(すなわち、for式を用いる事で同様の記述が行える)。

val implicitCrossJoin = for {
  c <- coffees
  s <- suppliers
} yield (c.name, s.name)
// compiles to SQL:
//   select x2."COF_NAME", x3."SUP_NAME"
//     from "COFFEES" x2, "SUPPLIERS" x3

もし結合の際にフィルタリングを行ったのなら、これは暗黙的な内部結合(inner join)となる。

val implicitInnerJoin = for {
  c <- coffees
  s <- suppliers if c.supID === s.id
} yield (c.name, s.name)
// compiles to SQL:
//   select x2."COF_NAME", x3."SUP_NAME"
//     from "COFFEES" x2, "SUPPLIERS" x3
//     where x2."SUP_ID" = x3."SUP_ID"

このような暗黙的結合は、ScalaコレクションのflatMapを扱うのと同様の意味合いを持つ。

明示的結合は適切なjoin関数を呼び出す事で実行出来る。

val explicitCrossJoin = for {
  (c, s) <- coffees innerJoin suppliers
} yield (c.name, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     inner join "SUPPLIERS" x3
...
val explicitInnerJoin = for {
  (c, s) <- coffees innerJoin suppliers on (_.supID === _.id)
} yield (c.name, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     inner join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitLeftOuterJoin = for {
  (c, s) <- coffees leftJoin suppliers on (_.supID === _.id)
} yield (c.name, s.name.?)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     left outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitRightOuterJoin = for {
  (c, s) <- coffees rightJoin suppliers on (_.supID === _.id)
} yield (c.name.?, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     right outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitFullOuterJoin = for {
  (c, s) <- coffees outerJoin suppliers on (_.supID === _.id)
} yield (c.name.?, s.name.?)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     full outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"

ここでは外部結合において.?といったものを用いている。これは、このような結合ではnull値が新たに追加されてしまうため、そのような値に対しOption値が取得される事を保証するためである。(左外部結合、右外部結合においても同様である)

リレーショナルデータベースによってサポートされた一般的な結合処理に加えて、Slickでは2つのクエリのペアワイズ結合を作成するzip結合というものを提供している。これはScalaコレクションにおいてzipzipWith関数を用いた処理と同様の意味合いを持つものである。

val zipJoinQuery = for {
  (c, s) <- coffees zip suppliers
  } yield (c.name, s.name)
...
val zipWithJoin = for {
  res <- coffees.zipWith(suppliers, (c: Coffees, s: Suppliers) => (c.name, s.name))
} yield res

ある種のzip結合はzipWithIndexにより提供される。これはクエリの結果を0から始まる無限数列とzipしたものとなる。そのような数列についてはSQLデータベースでは表す事が出来ず、Slickでも現在ではサポートしていない。しかし、行番号(row number)関数を利用する事でSQLにおいてzipクエリの結果については表す事が出来る。ゆえにzipWithIndexは原子的なオペレータとしてサポートされているのである。

val zipWithIndexJoin = for {
  (c, idx) <- coffees.zipWithIndex
} yield (c.name, idx)

Unions 

両立可能な2つのクエリは、++(もしくはunionAll)やunionオペレータを用いる事で連結する事が出来る。

val q1 = coffees.filter(_.price < 8.0)
val q2 = coffees.filter(_.price > 9.0)
...
val unionQuery = q1 union q2
// compiles to SQL (simplified):
//   select x8."COF_NAME", x8."SUP_ID", x8."PRICE", x8."SALES", x8."TOTAL"
//     from "COFFEES" x8
//     where x8."PRICE" < 8.0
//   union select x9."COF_NAME", x9."SUP_ID", x9."PRICE", x9."SALES", x9."TOTAL"
//     from "COFFEES" x9
//     where x9."PRICE" > 9.0
...
val unionAllQuery = q1 ++ q2
// compiles to SQL (simplified):
//   select x8."COF_NAME", x8."SUP_ID", x8."PRICE", x8."SALES", x8."TOTAL"
//     from "COFFEES" x8
//     where x8."PRICE" < 8.0
//   union all select x9."COF_NAME", x9."SUP_ID", x9."PRICE", x9."SALES", x9."TOTAL"
//     from "COFFEES" x9
//     where x9."PRICE" > 9.0

重複した値を弾くunionと違って、++は、より効率的な個々のクエリの結果を、単純に連結させる。

Aggregation 

最も単純な集合操作は、単一カラムを返却するQueryからプリミティブな値(大抵は数値型)を計算させる事で取得を行う。

val q = coffees.map(_.price)
...
val q1 = q.min
// compiles to SQL (simplified):
//   select min(x4."PRICE") from "COFFEES" x4
...
val q2 = q.max
// compiles to SQL (simplified):
//   select max(x4."PRICE") from "COFFEES" x4
...
val q3 = q.sum
// compiles to SQL (simplified):
//   select sum(x4."PRICE") from "COFFEES" x4
...
val q4 = q.avg
// compiles to SQL (simplified):
//   select avg(x4."PRICE") from "COFFEES" x4

これらの集合クエリはコレクションではなく、スカラー値を返却する事に注意して欲しい。いくつかの集合関数は恣意的なクエリにより定義がなされている。

val q1 = coffees.length
// compiles to SQL (simplified):
//   select count(1) from "COFFEES"
...
val q2 = coffees.exists
// compiles to SQL (simplified):
//   select exists(select * from "COFFEES")

groupBy関数によりグルーピングは行なわれる。これはScalaコレクションに対する操作と同じ意味を持つ。

val q = (for {
  c <- coffees
  s <- c.supplier
} yield (c, s)).groupBy(_._1.supID)
...
val q2 = q.map { case (supID, css) =>
  (supID, css.length, css.map(_._1.price).avg)
}
// compiles to SQL:
//   select x2."SUP_ID", count(1), avg(x2."PRICE")
//     from "COFFEES" x2, "SUPPLIERS" x3
//     where x3."SUP_ID" = x2."SUP_ID"
//     group by x2."SUP_ID"

ここで、中間クエリであるqはネストされた型Queryの値を保持している。つまり、クエリを実行する際にはネストされたコレクションが現れる。これは現在サポートがされていない。それゆえ、q2において行なわれるようにそれらの値(もしくは個々のカラム)をまとめることで、ネストされたクエリを即座に平滑化する必要がある。

Querying 

クエリはInvokerトレイト(もしくはパラメータが無い場合にはUnitInvoker)において定義された関数を用いて実行される。Queryに対する暗黙的な変換が存在しているため、直接的にQueryを実行できるのである。最も一般的な利用法として、listtoのような特定の関数を用いて、適切にコレクションの値を結果として読み込み事がある。

val l = q.list
val v = q.buildColl[Vector]
val invoker = q.invoker
val statement = q.selectStatement

このスニペットは暗黙的な変換関数を呼び出す事無しに、どのようにして手動でinvokerに対する参照を取得するのかを示している。

クエリを実行する全ての関数は暗黙的なSessionを必要とする。もちろん明示的にSessionを渡してあげてもよい。

val l = q.list()(session)

もし単一の結果値が欲しいのなら、firstfirstOptionといった関数を用いる事が出来る。foreachfoldLeftelementsといった関数はScalaコレクションに全てのデータをコピーしたりせずに、結果をイテレートさせる事が出来る。

Deleting 

データの削除はクエリの実行と同じように処理させる。削除したいデータを取得するクエリを書いた後に、delete関数を呼び出せば良い。Queryからdelete関数と自己参照用のdeleteInvokerを提供するDeleteInvokerへの暗黙的な変換が存在している。

val affectedRowsCount = q.delete
val invoker = q.deleteInvoker
val statement = q.deleteStatement

削除のためのクエリは単一のテーブルからデータを取得すべきだ。どんな射影も無視されるだろう(常に行を丸々削除する)。

Inserting 

データの挿入は単一のテーブルに対し、カラムの射影に基づいて実行される。テーブルを直接用いる際には、挿入はテーブルの*射影関数を用いて実行する。挿入時にテーブルのカラムをいくつか省くと、データベースはテーブル定義に基づき、デフォルト値を利用する。明示的なデフォルト値が無い場合には、型特有なデフォルト値を用いる。挿入に対する全ての関数はInsertInvokerFullInsertInvokerにおいて定義がなされている。

coffees += ("Colombian", 101, 7.99, 0, 0)
coffees ++= Seq(
  ("French_Roast", 49, 8.99, 0, 0),
  ("Espresso",    150, 9.99, 0, 0)
)
...
// "sales" and "total" will use the default value 0:
coffees.map(c => (c.name, c.supID, c.price)) += ("Colombian_Decaf", 101, 8.99)
val statement = coffees.insertStatement
val invoker = coffees.insertInvoker
// compiles to SQL:
//   INSERT INTO "COFFEES" ("COF_NAME","SUP_ID","PRICE","SALES","TOTAL") VALUES (?,?,?,?,?)

もしAutoIncなカラムが挿入操作において含まれていたなら、暗黙的に無視され、データベースは適切な値を生成しようとする。このような場合において、自動生成された主キーのカラムを返却して欲しいと思うだろう。デフォルトでは、+=関数は変更の合った行数(通常は1)を返却し、++=関数は蓄積したOptionの数を返却する(もしデータベースシステムがカウントを提供しなければ、Noneになるため)。もし特定のカラムを返却させたいのなら、returning関数を用いて変更する事が出来る。+=からは単一値もしくはタプルを、+==からはそれらの値のSeqを返す事が出来る。

val userId =
  (users returning users.map(_.id)) += User(None, "Stefan", "Zeiger")

ちなみに、多くのデータベースシステムではテーブルの自動インクリメントされる主キーを返却する事を許可している。もし他のカラムを返却しようとしたなら、(データベースがサポートしていない場合にも)SlickExceptionが実行時(at runtime)に投げられる。

クライアント側から挿入されるデータの代わりに、Queryによって作成されたデータもしくはデータベースサーバにおいて実行されたスカラー表現を挿入する事もできる。

class Users2(tag: Tag) extends Table[(Int, String)](tag, "users2") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = (id, name)
}
val users2 = TableQuery[Users2]
users2.ddl.create
users2 insert (users.map { u => (u.id, u.first ++ " " ++ u.last) })
users2 insertExpr (users.length + 1, "admin")

このような場合では、AutoIncカラムは無視されない。

Updating 

データの更新は、更新するデータを取得し、新たなデータに差し替えるクエリを記述する事で行える。クエリは単一のテーブルから選択された(計算のされていない)カラムが返却されるべきである。更新に関連する関数はUpdateInvokerにおいて定義がなされている。

val q = for { c <- coffees if c.name === "Espresso" } yield c.price
q.update(10.49)
...
val statement = q.updateStatement
val invoker = q.updateInvoker
...
// compiles to SQL:
//   update "COFFEES" set "PRICE" = ? where "COFFEES"."COF_NAME" = 'Espresso'

今現在、スカラー表現やデータベースに存在するデータを変換して用いる更新処理を行う方法は無い。

Compiled Queries 

データベースに対する処理は基本的にいくつかのパラメータに依存している(これはデータベースから探索を行いたいデータのIDの事などである)。クエリを実行するたびに、パラメータを入れたQueryオブジェクトを作成するような関数をしばしば記述する。しかし、これはSlickにおいてクエリをコンパイルしなおすコストを増長させる。そこで、このようなパラメータが固定されたクエリについて、事前コンパイルを行うことでより効率化する事が出来る。

def userNameByIDRange(min: Column[Int], max: Column[Int]) =
  for {
    u <- users if u.id >= min && u.id < max
  } yield u.first

val userNameByIDRangeCompiled = Compiled(userNameByIDRange _)
...
// The query will be compiled only once:
val names1 = userNameByIDRangeCompiled(2, 5).run
val names2 = userNameByIDRangeCompiled(1, 3).run
// Also works for .update and .delete

これはColumnパラメータ(もしくはカラムのレコード)を取ったり、Queryオブジェクトやクエリを返却する全ての関数において上手く機能する。CompiledやそのサブクラスのAPIドキュメントを見ると、コンパイルされたクエリの構成についての詳細を知ることが出来る。

コンパイルされたクエリをクエリ処理、更新、削除といった処理に対して用いる事ができる。

Slick 1.0の後方互換のために、ParametersオブジェクトのflatMapを呼ぶことで依然コンパイルされたクエリを作る事が出来る。多くの場合において、単一のfor式を書くことでコンパイルされたクエリを作る事が出来るだろう。

val userNameByID = for {
  id <- Parameters[Int]
  u <- users if u.id is id
} yield u.first
...
val name = userNameByID(2).first
...
val userNameByIDRange = for {
  (min, max) <- Parameters[(Int, Int)]
  u <- users if u.id >= min && u.id < max
} yield u.first
...
val names = userNameByIDRange(2, 5).list
Fork me on GitHub