Slick 3.0.0 documentation - 06 Schemas

Permalink to Schemas — Slick 3.0.0 documentation

スキーマ 

この章では、既存のデータベースを持たない新しいアプリケーションを作る際、どのようにしてScalaのコードでデータベーススキーマを記述するのかを説明する。もしデータベーススキーマを既に持っているのなら、code generatorを利用することで、手で書く手間は省ける。

Table Rows 

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

全てのテーブルはデフォルトの射影として*メソッドを定義している。これは、クエリの結果として列を返す際に、あなたがどんな情報を求めているのかを説明するためのものである。Slickの*射影は、データベース内のカラムと一致している必要は無い。何かしらの計算結果を追加したり、いくつかのカラムを省いて使っても良い。*射影の結果は、Tableの型引数と一致する必要があり、これはマッピングされた何かしらのクラスか、カラムが用いられることになるだろう。

もしデータベースが schema names を必要とするなら、テーブル名の前にその名前を明示して欲しい。

class Coffees(tag: Tag)
  extends Table[(String, Int, Double, Int, Int)](tag, Some("MYSCHEMA"), "COFFEES") {
  //...
}

Table Query 

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)
}

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]

これはapplyunapplyを持つケースクラス用に最適化されているが、任意のマッピングを行う事も可能である。適切に型を推測してくれるタプルを生成してくれる.shapedという便利なメソッドもある。任意のマッピングを行う場合には、マッピング用の型アノテーションを適宜書いて欲しい。

ケースクラスのコンパニオンオブジェクトを手で書いている場合には、Scalaの機能に合うように実装が行われている場合にのみ、.tupledは上手く動作する。他にも(User.apply _).tupledなどを使ったりも出来るだろう。 SI-3664SI-4808も目を通しておいて欲しい。

Constraints 

外部キーは、TableのforeignKeyによって定義される。第一引数には、制約名、関連カラム、関連テーブルの3つを渡す。続く第二引数は、関連テーブルの紐付けるカラムに加えて、OnUpdateOnDeleteのような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メソッドをオーバーライドする事でカスタマイズ可能だ。

Data Definition Language 

テーブルのDDLステートメントはそのテーブルのTableQueryschemaメソッドを基に作成される。複数のDDLオブジェクトは++メソッドにより1つのDDLオブジェクトに結合出来る。これはcreate時もdrop時も全ての制約に対し、たとえ循環依存がテーブル間に存在したとしても、正しい挙動をするように実行されるものとなる。createdropメソッドは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)
Fork me on GitHub