Slick 3.0.0 documentation - 10 Plain SQL Queries
Permalink to Plain SQL Queries — Slick 3.0.0 documentation
もしかすると、高レベルに抽象化されてサポートされたオペレーションに対し、SQLコードをそのまま書きたいといった要求があるかもしれない。そのような場合には、低レベルなJDBCのAPIを用いるのではなく、Slickが提供するScalaベースの Plain SQL を利用して欲しい。
Note
本章の残りでは、Slick Plain SQL Queries templateをベースに説明を行う。Activatorからテンプレートを落としてきて、直接編集したり実行しながら読んでみて欲しい。
データベースのコネクションは、いつもと同じように開かれる。全ての Plain SQL DBIOActions内で実行される。これは複数のアクションを組み合わせたものする事も可能である。
Slickの Plain SQL はsql
、sqlu
、tsql
という文字列の補間(string interpolation)を通して組み立てることが出来る。これらはSlickドライバからapi._
をインポートする事で利用可能となる。
import slick.driver.H2Driver.api._
最も簡単な使用法としては、以下のようなメソッドの中で利用しているように、sqlu
の中にSQLコードをそのまま書いてしまうことだ。
def createCoffees: DBIO[Int] =
sqlu"""create table coffees(
name varchar not null,
sup_id int not null,
price double not null,
sales int not null,
total int not null,
foreign key(sup_id) references suppliers(id))"""
...
def createSuppliers: DBIO[Int] =
sqlu"""create table suppliers(
id int not null primary key,
name varchar not null,
street varchar not null,
city varchar not null,
state varchar not null,
zip varchar not null)"""
...
def insertSuppliers: DBIO[Unit] = DBIO.seq(
// Insert some suppliers
sqlu"insert into suppliers values(101, 'Acme, Inc.', '99 Market Street', 'Groundsville', 'CA', '95199')",
sqlu"insert into suppliers values(49, 'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', '95460')",
sqlu"insert into suppliers values(150, 'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', '93966')"
)
sqlu
補間子は、結果の代わりに列の数を返すDMLステートメントとして用いられる。それゆえ、sqlu
を用いた場合は返り値の型がDBIO[Int]
となる。
クエリに注入される変数や表現は、クエリ文字列の中でバインド変数などで表される。クエリ文字列に直接変数を入れることはしない。このような対応は、SQLインジェクションをなくすためにある。以下の例を見て欲しい。
def insert(c: Coffee): DBIO[Int] =
sqlu"insert into coffees values (${c.name}, ${c.supID}, ${c.price}, ${c.sales}, ${c.total})"
このメソッドにより生成されるSQLステートメントは、常に同じものになる。
insert into coffees values (?, ?, ?, ?, ?)
この種のコードに役立つ便利なDBIO.sequenceコンビネータは以下のように利用できる。
val inserts: Seq[DBIO[Int]] = Seq(
Coffee("Colombian", 101, 7.99, 0, 0),
Coffee("French_Roast", 49, 8.99, 0, 0),
Coffee("Espresso", 150, 9.99, 0, 0),
Coffee("Colombian_Decaf", 101, 8.99, 0, 0),
Coffee("French_Roast_Decaf", 49, 9.99, 0, 0)
).map(insert)
...
val combined: DBIO[Seq[Int]] = DBIO.sequence(inserts)
combined.map(_.sum)
与えられた順序でデータベースのI/Oアクションを直列に実行するシンプルなDBIO.seqとは異なり、DBIO.sequenceは個々のアクションの結果を保護するために、Seq[DBIO[T]]
をDBIO[Seq[T]]
へ変換する。これは挿入時に影響のあった列の数を数え上げる際などに用いられている。
以下のコードでは、ステートメントにより得られた結果を返却するsql
補間子を利用している。sql
補間子自身はDBIO
の値を生成したりはしない。これは、.as
というメソッドを返り値となる型を組み合わせて呼び出す必要がある。
sql"""select c.name, s.name
from coffees c, suppliers s
where c.price < $price and s.id = c.sup_id""".as[(String, String)]
この結果の型は、DBIO[Seq[(String, String)]]
となる。as
を呼び出す際には、結果から要求する型の値を抽出するGetResultパラメータを暗黙的に必要としている。基本的なJDBCの型やOption、タプルなどに対するGetResult
は予め定義されている。それ以外の型に対するGetResult
は、各自で定義して欲しい。
// 適当なケースクラス
case class Supplier(id: Int, name: String, street: String, city: String, state: String, zip: String)
case class Coffee(name: String, supID: Int, price: Double, sales: Int, total: Int)
...
// 結果を抽出するためにGetResult
implicit val getSupplierResult = GetResult(r => Supplier(r.nextInt, r.nextString, r.nextString,
r.nextString, r.nextString, r.nextString))
implicit val getCoffeeResult = GetResult(r => Coffee(r.<<, r.<<, r.<<, r.<<, r.<<))
GetResult[T]
はPositionedResult => T
という関数の単なるラッパーにすぎない。Supplier
のための暗黙的なGetResult
は、列からInt
かString
の値を読み出すために、明示的なPositionedResult
を用いている。2個めのCoffee
の例では、期待する型を自動的に導出しようと試みる<<
というショートカットメソッドを利用している(コンストラクタの呼び出しに対して明らかに型が導出出来る場合にのみ利用可能)。
パラメータはSQLステートメントに対してバインド変数を用いて挿入されるわけだが、動的に生成されたSQLコードを呼び出す際などでは、もしかすると直接ステートメントの中にリテラルを書く必要が生じるかもしれない。このような場合には以下の例のように、全ての補間子の中で$
の代わりに#$
を用いて変数をバインドしてあげれば良い。
val table = "coffees"
sql"select * from #$table where name = $name".as[Coffee].headOption
今まで見てきた補間子は、SQLステートメントを実行時に構築する。これはステートメントを構築する安全で簡単な方法となっている一方、単なる埋め込み文字列にしかならない。もしステートメントにシンタックスエラーがあったり、データベースとScalaのコードに何かしら型の違いがあったする場合にも、コンパイル時に検出が出来なく、非常に残念である。そのような場合には、sql
補間子の代わりにtsql
補間子を使う事を検討してみて欲しい。
def getSuppliers(id: Int): DBIO[Seq[(Int, String, String, String, String, String)]] =
tsql"select * from suppliers where id > $id"
tsql
は.as
を呼び出す必要無しに、直接DBIOAction
を生成する。
tsql
を利用する際は、SQLコンパイラをデータベースにアクセスさせるために、コンパイル時に解決できる設定を提供してあげる必要がある。これはStaticDatabaseConfigアノテーションを利用して明示する。
@StaticDatabaseConfig("file:src/main/resources/application.conf#tsql")
上の例だと、application.conf
というファイルにおける、"tsql"
というパスを指し示しており、ここにはDatabase
の設定だけではなく、StaticDatabaseConfigオブジェクトのための適切な設定を記述しなくてはならない。
Note
パスを省いたり、URLのフラグメントのみを指定したりすると、クラスパスにある中から
application.conf
を見つけようとする。また、resource:
というURLスキーマを利用しても良いが、いずれにしても実行時のクラスパスと異なり、コンパイラ時のクラスパスからそれらは見えるようにする必要がある。ビルドツールによっては設定が出来ないかもしれないため、基本的にはfile:
のURLスキーマで相対パスを指定するのが良い。
実行時に、設定のされたDatabaseConfigを取得させても構わない。
val dc = DatabaseConfig.forAnnotation[JdbcProfile]
import dc.driver.api._
val db = dc.db
ここでは、基本的なapi._
というインポートとDatabase
を利用している。同じ設定を用いさせることは特に強制しておらず、SlickドライバとDatabase
を他の方法で実行時に渡しても良いし、コンパイル時のチェックのみにStaticDatabaseConfig
を利用するといった方法も1つの選択肢として考えられる。