Slick 2.0.0 documentation - 06 Schemas

Permalink to Schemas — Slick 2.0.0 documentation

Schemas 

ここでは、Lifted Emebedding APIにおいて、データベーススキーマをどのようにして取り扱うのかということについて説明する。初めに、手でスキーマを記述する方法についての説明を行う。手で書く以外にもコードジェネレータを使うこともできる。

Tables 

型安全なクエリを扱う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において定義されている。

全てのテーブルではデフォルトの射影を表す*関数が必要になる。これはクエリを通して行を取り出す際に戻ってくるものが何になるべきかを示すものである。Slickの*射影はデータベースの*とは一致したものになる必要は無い。何かしらの計算を行った新たなカラムを足してもいいし、特定のカラムを省いても良いし好きにして良い。*射影と一致するような持ち上げられていない(non-lifted)型はTableへと型パラメータとして与えられる。例えば、マッピングのないテーブルにおいて、これは単一のカラム型もしくはカラムのタプル型になるだろう。

Mapped Tables 

両方向マッピングを行う<>オペレータを用いる事で、*射影に対し、自由な型をテーブルへマッピングする事が出来る。

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型を返すシンプルなapplyunapply関数のある)ケースクラスを用いる事で最適化されるが、自由なマッピング関数を用いても良い。この場合、適切な型を推測するのにタプルの.shapedを呼ぶのが役に立つ。一方で、マッピング関数に充分な型アノテーションを付与しても良いだろう。

Constraints 

外部キー制約はテーブルの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関数をオーバーライドする事でカスタマイズ出来る。

Data Definition Language 

テーブルのDDLステートメントは、TableQueryddl関数を用いて作成される。複数のDDLオブジェクトは++関数を用いて連結する事ができ、テーブル間にサイクルした依存関係が存在していたとしても、適切な順序で全てのテーブルを作成、削除する事が出来る。ステートメントはcreatedrop関数を用いて実行される。

val ddl = coffees.ddl ++ suppliers.ddl
db withDynSession {
  ddl.create
  //...
  ddl.drop
}

createStatementsdropStatements関数を用いると、実際に吐かれるSQLについて確認する事が出来る。

ddl.createStatements.foreach(println)
ddl.dropStatements.foreach(println)
Fork me on GitHub