Slick 2.0.0 documentation - 06 Schemas
Permalink to Schemas — Slick 2.0.0 documentation
ここでは、Lifted Emebedding APIにおいて、データベーススキーマをどのようにして取り扱うのかということについて説明する。初めに、手でスキーマを記述する方法についての説明を行う。手で書く以外にもコードジェネレータを使うこともできる。
型安全なクエリを扱うLifted Embedding APIを用いるには、データベーススキーマに対応するTable
クラスと、TableQuery
値を定義する必要がある。
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
def name = column[String]("COF_NAME", O.PrimaryKey)
def supID = column[Int]("SUP_ID")
def price = column[Double]("PRICE")
def sales = column[Int]("SALES", O.Default(0))
def total = column[Int]("TOTAL", O.Default(0))
def * = (name, supID, price, sales, total)
}
val coffees = TableQuery[Coffees]
全てのカラムはcolumn
関数を通して定義される。各カラムはScalaの型を持っており、アッパーケースで通常記述されるデータベース用のカラム名を持つ。以下に挙げるようなプリミティブ型はJdbcProfile
において、JDBCベースなデータベースのためにボクシングされた型が適応される。(各種データベースドライバーによって恣意的に割り当てられているものもある)
nullを許容するカラムはT
がサポートされたプリミティブ型である際に、Option[T]
を用いて表せば良い。ただし、このOptionに対する全ての操作は、ScalaのOption操作と異なり、データベースのnullプロパゲーションセマンティクスを用いてしまう事に注意して欲しい。特に、None === None
という式はNone
になる。これはSlickのメジャーリリースで将来的に変更されるかもしれない。
column関数には、カラム名の後にカラムのオプションを追加する事が出来る。適用出来るオブションはテーブルのO
オブジェクトを通して利用出来る。以下のようなオプションがJdbcProfile
において定義されている。
PrimaryKey
Default[T](defaultValue: T)
DBType(dbType: String)
String
型のカラムに対してDBType("VARCHAR(20")
を用いるなど)。
AutoInc
AutoInc
として適切にマークされているのかどうかをチェックする。
NotNull
, Nullable
Option
型がOption
型でないかといった違いからも決定される。一般的にこれらのオプションを使う理由は無い。
全てのテーブルではデフォルトの射影を表す*
関数が必要になる。これはクエリを通して行を取り出す際に戻ってくるものが何になるべきかを示すものである。Slickの*
射影はデータベースの*
とは一致したものになる必要は無い。何かしらの計算を行った新たなカラムを足してもいいし、特定のカラムを省いても良いし好きにして良い。*
射影と一致するような持ち上げられていない(non-lifted)型はTable
へと型パラメータとして与えられる。例えば、マッピングのないテーブルにおいて、これは単一のカラム型もしくはカラムのタプル型になるだろう。
両方向マッピングを行う<>
オペレータを用いる事で、*
射影に対し、自由な型をテーブルへマッピングする事が出来る。
case class User(id: Option[Int], first: String, last: String)
...
class Users(tag: Tag) extends Table[User](tag, "users") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def first = column[String]("first")
def last = column[String]("last")
def * = (id.?, first, last) <> (User.tupled, User.unapply)
}
val users = TableQuery[Users]
(Option
型を返すシンプルなapply
やunapply
関数のある)ケースクラスを用いる事で最適化されるが、自由なマッピング関数を用いても良い。この場合、適切な型を推測するのにタプルの.shaped
を呼ぶのが役に立つ。一方で、マッピング関数に充分な型アノテーションを付与しても良いだろう。
外部キー制約はテーブルのforeignKey
関数を用いて定義出来る。この関数は制約のための名前、ローカルカラム(もしくは射影、つまりここでは複合外部キーを定義出来る)、関連するテーブル、そしてテーブルから一致するカラムに対する関数を引数に取る。テーブルのためのDDLステートメントが作成される際、外部キー定義が追加される。
class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
def id = column[Int]("SUP_ID", O.PrimaryKey)
//...
}
val suppliers = TableQuery[Suppliers]
...
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
def supID = column[Int]("SUP_ID")
//...
def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
// compiles to SQL:
// alter table "COFFEES" add constraint "SUP_FK" foreign key("SUP_ID")
// references "SUPPLIERS"("SUP_ID")
// on update NO ACTION on delete NO ACTION
}
val coffees = TableQuery[Coffees]
データベースに定義された実際の制約とは独立して、joinなどで用いられるような関連データについてのナビゲーションとしても外部キーは用いる事が出来る。この目的において、結合されるデータを探すための便利関数を手動で定義させる。
def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
def supplier2 = suppliers.filter(_.id === supID)
主キー制約は外部キーと同じように、primaryKey
関数を用いる事で定義出来る。これは複合主キーを定義するのに便利なものとなっている。(column
関数のオプションであるO.PrimaryKey
では複合主キーは定義出来ない)
class A(tag: Tag) extends Table[(Int, Int)](tag, "a") {
def k1 = column[Int]("k1")
def k2 = column[Int]("k2")
def * = (k1, k2)
def pk = primaryKey("pk_a", (k1, k2))
// compiles to SQL:
// alter table "a" add constraint "pk_a" primary key("k1","k2")
}
またインデックスについても同様にindex
関数を用いて定義出来る。unique
パラメータが内場合にはユニークなものではない、として定義される。
class A(tag: Tag) extends Table[(Int, Int)](tag, "a") {
def k1 = column[Int]("k1")
def k2 = column[Int]("k2")
def * = (k1, k2)
def idx = index("idx_a", (k1, k2), unique = true)
// compiles to SQL:
// create unique index "idx_a" on "a" ("k1","k2")
}
全ての制約は、テーブルにおいて定義された適切な返却型を用いて、反射的に探索が行なわれる。このような挙動に対して、tableConstraints
関数をオーバーライドする事でカスタマイズ出来る。
テーブルのDDLステートメントは、TableQuery
のddl
関数を用いて作成される。複数のDDL
オブジェクトは++
関数を用いて連結する事ができ、テーブル間にサイクルした依存関係が存在していたとしても、適切な順序で全てのテーブルを作成、削除する事が出来る。ステートメントはcreate
とdrop
関数を用いて実行される。
val ddl = coffees.ddl ++ suppliers.ddl
db withDynSession {
ddl.create
//...
ddl.drop
}
createStatements
やdropStatements
関数を用いると、実際に吐かれるSQLについて確認する事が出来る。
ddl.createStatements.foreach(println)
ddl.dropStatements.foreach(println)