Slick 3.0.0 documentation - 06 Schemas
Permalink to Schemas — Slick 3.0.0 documentation
この章では、既存のデータベースを持たない新しいアプリケーションを作る際、どのようにしてScalaのコードでデータベーススキーマを記述するのかを説明する。もしデータベーススキーマを既に持っているのなら、code generatorを利用することで、手で書く手間は省ける。
型安全なクエリをScalaのAPIを通して利用するには、データベーススキーマに応じたTable
クラスを定義する必要がある。これは、テーブルの構造を表現するものである。
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)
}
全てのカラムは、column
メソッドを通して定義される。どのカラムもScalaの型と、データベースで利用されるカラム名を持つ(カラム名は一般的には大文字)。以下のプリミティブな型は、JdbcProfile
においてJDBCベースなデータベースのためのサポートがなされている(個々のデータベースドライバによっていくつかの制限が存在するが)。
Nullになりえるカラムについては、T
がプリミティブ型でサポートされている場合、Option[T]
で表現することが出来る。
Note
このOptionに対する全ての操作は、ScalaのOption操作と異なり、データベースのnullプロパゲーションセマンティクスを用いることになる点に注意して欲しい。特に、
None === None
という式はNone
になる。これはSlickのメジャーリリースで将来的に変更されるかもしれない。
カラム名の後ろには、column
の定義につけるオプションを付与する事ができる。適用可能なオプションは、テーブルのO
オブジェクトを通して利用出来る。以下のオプションが、JdbcProfile
用に定義されている。
PrimaryKey
Default[T](defaultValue: T)
DBType(dbType: String)
String
型のカラムに対して、DBType("VARCHAR(20)")
を明示して指定したりする。
AutoInc
NotNull
, Nullable
Option
かそうでないかでnullを許容するかを指定出来るため、一般的にはこのオプションは用いられない。
全てのテーブルはデフォルトの射影として*
メソッドを定義している。これは、クエリの結果として列を返す際に、あなたがどんな情報を求めているのかを説明するためのものである。Slickの*
射影は、データベース内のカラムと一致している必要は無い。何かしらの計算結果を追加したり、いくつかのカラムを省いて使っても良い。*
射影の結果は、Table
の型引数と一致する必要があり、これはマッピングされた何かしらのクラスか、カラムが用いられることになるだろう。
もしデータベースが schema names を必要とするなら、テーブル名の前にその名前を明示して欲しい。
class Coffees(tag: Tag)
extends Table[(String, Int, Double, Int, Int)](tag, Some("MYSCHEMA"), "COFFEES") {
//...
}
Table
クラスに対して、実際のデータベーステーブルを表すTableQuery
も必要になるだろう。
val coffees = TableQuery[Coffees]
TableQuery[T]
というシンプルなシンタックスはマクロであり、これはnew TableQuery(new T(_))
のようなテーブルのコンストラクタを呼び出すTableQueryのインスタンスとなる。
テーブルに関連する追加機能を提供するために、TableQuery
を継承しても良いだろう。
object coffees extends TableQuery(new Coffees(_)) {
val findByName = this.findBy(_.name)
}
*
射影の結果を独自の型にマッピングしたいのなら、<>
オペレータを利用して双方向マッピングを定義してあげると良い。
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]
これはapply
とunapply
を持つケースクラス用に最適化されているが、任意のマッピングを行う事も可能である。適切に型を推測してくれるタプルを生成してくれる.shaped
という便利なメソッドもある。任意のマッピングを行う場合には、マッピング用の型アノテーションを適宜書いて欲しい。
ケースクラスのコンパニオンオブジェクトを手で書いている場合には、Scalaの機能に合うように実装が行われている場合にのみ、.tupled
は上手く動作する。他にも(User.apply _).tupled
などを使ったりも出来るだろう。 SI-3664やSI-4808も目を通しておいて欲しい。
外部キーは、TableのforeignKeyによって定義される。第一引数には、制約名、関連カラム、関連テーブルの3つを渡す。続く第二引数は、関連テーブルの紐付けるカラムに加えて、OnUpdate
やOnDelete
のようなForeignKeyActionに関するものを指定できる。ForeignKeyAction
のデフォルト値はNoActionとなっている。テーブルの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, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Cascade)
// compiles to SQL:
// alter table "COFFEES" add constraint "SUP_FK" foreign key("SUP_ID")
// references "SUPPLIERS"("SUP_ID")
// on update RESTRICT on delete CASCADE
}
val coffees = TableQuery[Coffees]
データベースに定義された制約とは別に、join時に利用出来る外部キーを用意する事もできる。この外部キーは、他のテーブルから関連を取得する便利メソッドとして利用することが出来る。
def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Cascade)
def supplier2 = suppliers.filter(_.id === supID)
主キー制約はprimaryKey
というメソッドを用いる事で同様に定義出来る。これは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
のschema
メソッドを基に作成される。複数のDDL
オブジェクトは++
メソッドにより1つのDDL
オブジェクトに結合出来る。これはcreate時もdrop時も全ての制約に対し、たとえ循環依存がテーブル間に存在したとしても、正しい挙動をするように実行されるものとなる。create
やdrop
メソッドはDDLステートメントを実行するActionを生成する。
val schema = coffees.schema ++ suppliers.schema
db.run(DBIO.seq(
schema.create,
//...
schema.drop
))
statemens
メソッドを用いる事で、SQLのコードを取得出来る。スキーマのActionは、1つ以上のステートメントを生成するようになっている。
schema.create.statements.foreach(println)
schema.drop.statements.foreach(println)