ScalaQueryでカラムをEnumerationにマッピングする


こんにちは、東です。
お仕事でScalaからRDBをアクセスするのにScalaQueryを使っています。
ScalaQueryで「格納できる値が制限されているカラム」を素敵に取り扱う方法についてご紹介します。

条件設定

下記のような社員管理用の簡単なテーブルを考えます。

表:社員
社員番号 氏名 部署名
CREATE TABLE employee (
  id         INT,
  name       CHARACTER VARYING,
  department CHARACTER VARYING
);

部署名には'FINANCE'や'RESEARCH'などの英語部署名が文字列で格納されるとします。
また、この会社には'FINANCE'、'RESEARCH'、'MARKETING'の3つの部署しかなく、これ以外の文字列は登録されないものとします。

素直な実装

上記のテーブルにアクセスするコードをScalaQueryで素直に実装すると、以下のようになります。
部署名はStringにマッピングします。

import org.scalaquery.ql.basic.BasicTable

case class Employee(id: Int, name: String, department: String)

val employeeTable = new BasicTable[Employee]("employee") {
  def id = column[Int]("id")
  def name = column[String]("name")
  def department = column[String]("department")
  def * = id ~ name ~ department <> (Employee, Employee.unapply _)
}

この場合の使い方はこんな感じです。

import org.scalaquery.ql.basic.BasicDriver.Implicit._
import org.scalaquery.session.Database
import org.scalaquery.session.Database.threadLocalSession

val db = Database.forURL(...)
db withSession {
  employeeTable.insert(Employee(1, "Fernando",  "FINANCE"))
  employeeTable.insert(Employee(2, "Sebastian", "RESEARCH"))
  employeeTable.insert(Employee(3, "Jenson",    "MARKETING"))
  employeeTable.insert(Employee(4, "Mark",      "RESEARCH"))
  employeeTable.insert(Employee(5, "Lewis",     "MARKETING"))
  employeeTable.insert(Employee(6, "Felipe",    "FINANCE"))
  employeeTable.insert(Employee(7, "Sergio",    "sales"))
  // ↑salesなんて部署はないのに...
}

部署名をStringで指定しているのが気持ち悪いです。
Employeeのコンストラクタの第三引数にはどんな文字列でも指定できてしまうので、想定外のデータをDBに入れることができてしまいます。
もちろん、SQLでCHECK制約を付与しておけば実行時にエラーとすることができますが、そもそも想定外のデータは指定できないようにする方がより良い設計です。

素敵な実装

さて、素敵な実装方法です。
まず、部署名をEnumerationで定義します(→DepartmentType.Value)。
ここで指定できる値を列挙しておきます。
さらにMappedTypeMapper.baseを使って、DepartmentType.ValueをStringにマッピングします。

import org.scalaquery.ql.MappedTypeMapper

object DepartmentType extends Enumeration {
  val FINANCE, MARKETING, RESEARCH = Value
}

implicit val departmentTypeMapper =
  MappedTypeMapper.base[DepartmentType.Value, String](
    _.toString,
    DepartmentType.withName(_))

MappedTypeMapper.baseの第一引数にはDepartmentType.ValueからStringへの変換方法、第二引数にはStringからDepartmentType.Valueへの変換方法を指定します。

こうすることで、以下のようにカラムにDepartmentType.Valueをマッピングすることができるようになります。
ハイライトの行が、素直な実装と差異のある箇所です。

import org.scalaquery.ql.basic.BasicTable

case class Employee(id: Int, name: String, department: DepartmentType.Value)

val employeeTable = new BasicTable[Employee]("employee") {
  def id = column[Int]("id")
  def name = column[String]("name")
  def department = column[DepartmentType.Value]("department")
  def * = id ~ name ~ department <> (Employee, Employee.unapply _)
}

こうしておけば、間違った部署名を指定することができなくなります。

import org.scalaquery.ql.basic.BasicDriver.Implicit._
import org.scalaquery.session.Database
import org.scalaquery.session.Database.threadLocalSession

val db = Database.forURL(...)
db withSession {
  employeeTable.insert(Employee(1, "Fernando",  DepartmentType.FINANCE))
  employeeTable.insert(Employee(2, "Sebastian", DepartmentType.RESEARCH))
  employeeTable.insert(Employee(3, "Jenson",    DepartmentType.MARKETING))
  employeeTable.insert(Employee(4, "Mark",      DepartmentType.RESEARCH))
  employeeTable.insert(Employee(5, "Lewis",     DepartmentType.MARKETING))
  employeeTable.insert(Employee(6, "Felipe",    DepartmentType.FINANCE))
  employeeTable.insert(Employee(7, "Sergio",    DepartmentType.sales))
  // ↑コンパイルエラー!
}

単純ですが、「営業部の綴りってどうだっけ?」なんて悩まずに済んで、実装が楽になるのでとてもお勧めです。

参考

MappedTypeMapperが導入される時の議論はこちら
0.9.2に導入されたときの解説はこちら
ScalaQueryのサイトからリンクされているドキュメントでは「Using Custom Data Types」(p.33)として紹介されています。


This entry was posted in 技術. Bookmark the permalink.