Anorm
Overview
Anorm は v2.4 以降、playframework とは別プロジェクトになっている。
libraryDependencies ++= Seq(
jdbc,
//anorm, // v2.3
"com.typesafe.play" %% "anorm" % "2.5.0" // v2.4 or higher
)
SQL
ヘルパーメソッド SQL により、SQL文字列が SqlQuery に変換される。
def SQL(stmt: String): SqlQuery =
SqlStatementParser.parse(stmt).map(ts => SqlQuery.prepare(ts, ts.names)).get
SqlStatementParser
SqlStatementParser.parse により、SQL文字列が Try[TokenizedStatement] に変換される。
val ts = SqlStatementParser.parse(
"SELECT * FROM users WHERE id = {id} AND status = ?").get
println(ts.names)
// List("id")
println(ts.tokens)
// List(
// TokenGroup(List(StringToken("SELECT * FROM users WHERE id = ")), Some(id)),
// TokenGroup(List(StringToken(" AND status = ?")), None)
// )
TokenizedStatement は別モジュール anorm-tokenizer に含まれている。パッケージプライベート private[anorm] のため直接扱うことはない。
SqlQuery
SqlQuery 自体は、単に TokenizedStatement を保持しておくだけの箱である。Implicit conversion によりSqlQuery#asSimple が呼ばれ SimpleSql のメソッドが利用可能になる。
都度、ヘルパーメソッド SQL を呼ぶと変換コストがかかってしまうので、変換済みの SqlQuery インスタンスを保持しておくようにする。
import anorm._
import anorm.SqlParser._
val stmt = "SELECT email FROM users WHERE id = {id}"
val sql = SQL(stmt)
val parser = get[String]("email").*
// NG
for (n <- 1 to 10000) {
SQL(stmt).on('id -> n).as(parser)
}
// OK
for (n <- 1 to 10000) {
sql.on('id -> n).as(parser)
}
WithResult
WithResult は、SELECT 結果を得るメソッドを提供する。scala-arm の resource.ManagedResource により、自動的に java.sql.(PreparedStatement|ResultSet) がクローズされる。
as
as メソッドに ResultSetParser を渡すことで、結果セットを任意のモデルに変換できる。
import anorm._
import anorm.SqlParser._
val parser: RowParser[(Long, String)] =
get[Long]("id") ~ get[String]("email") map {
case id ~ email => (id -> email)
}
val usersList: List[(Long, String)] =
SQL("SELECT * FROM users WHERE status = 1 ORDER BY id").as(parser.*)
withResult
withResult メソッドを使えば、結果を一度にメモリに入れることなく一行づつ処理できる。Loan pattern でカーソルを受け取る関数 Option[Cursor] => T を渡す。List[Row] を組み立てる場合を例にすると、 アキュムレータを使った再帰関数を部分適用して渡せば良い。
@annotation.tailrec
def go(op: Option[Cursor], acc: List[Row]): List[Row] =
op match {
case Some(c) => go(c.next, acc :+ c.row)
case None => acc
}
val result: Either[List[Throwable], List[Row]] =
SQL("SELECT * FROM users WHERE status = 1 ORDER BY id")
.withResult(go(_, List.empty[Row]))
fold
通常は fold メソッドを使うと良い。内部で withResult を呼んでいる。
val result: Either[List[Throwable], List[Row]] =
SQL("SELECT * FROM users ORDER BY id")
.fold(List.empty[Row]) { (acc, row) => acc :+ row }
foldWhile
foldWhile を使えば、カーソル走査を中断することができる。
val result: Either[List[Throwable], List[Row]] =
SQL("SELECT * FROM users ORDER BY id")
.foldWhile(List.empty[Row]) { (acc, row) =>
if (acc.size < 10) (acc :+ row, true)
else (acc, false)
}
RowParser
RowParser[+A] の実体は、関数 Row => SqlResult[A] である。
ヘルパーメソッド SqlParser.get[T] で、指定のカラム名またはカラム番号の RowParser を得られる。
import anorm.SqlParser._
val idColumnParser = get[Long]("id")
val emailColumnParser = get[String]("email")
val thirdColumnParser = get[Int](3)
一般的な型のヘルパーメソッドが定義されているので、通常はこれらを使う。
import anorm.SqlParser._
val idColumnParser = long("id")
val emailColumnParser = str("email")
val thirdColumnParser = int(3)
bool:get[Boolean](byte|short|int|long):get[(Byte|Short|Int|Long)]float:get[Float]double:get[Double]str:get[String]Stringjava.sql.Clobdate:get[java.util.Date]java.sql.DateLong{ def getTimestamp: java.sql.Timestamp }binaryStream:get[java.io.InputStream]Array[Byte]Stringjava.io.InputStreamjava.sql.Blob
~ で RowParser を連結することで、複数カラムの RowParser を作成できる。
val parser: RowParser[Long ~ String ~ Int ~ java.util.Date] =
long("id") ~ str("email") ~ int("status") ~ date("birthday")
val userParser: RowParser[User] = parser map {
case id ~ email ~ status ~ birthday =>
User(id, email, status, new java.util.Date(birthday.getTime))
}
正確には連結しているように見えるだけで、case class ~[+A, +B](_1: A, _2: B) がネストしているだけである。
final case class ~[+A, +B](_1: A, _2: B)
val tupleLike: ~[~[Long, String], Int] = new ~(new ~(123L, "foo@example.net"), 1)
val tuple: (Long, String, Int) = tupleLike match {
case ~(~(id, email), status) => (id, email, status)
}
ケースクラスとしての anorm.~[+A, +B] と、RowParser のメソッド ~[B](p: RowParser[B]): RowParser[A ~ B] を混同しがちである。
// NG: ~[RowParser[Long], RowParser[String]]
val ng = new ~(SqlParser.long("id"), SqlParser.long("email"))
// OK: RowParser[~[Long, String]]
val ok = SqlParser.long("id") ~ SqlParser.long("email")
- 前者は、単に擬似タプルとしての
~[A, B]であり、RowParserではない。 - 後者は、
RowParser[A]のメソッドにより、別のRowParser[B]を加えて生成された、新たなRowParser[A ~ B]である。
RowParser[+A] の実体は、関数 Row => SqlResult[A] であるので
Row => SqlResult[A]が- 引数
Row => SqlResult[B]を得て Row => SqlResult[A ~ B]を生成する
と考えると理解しやすい。SqlResult[+A]#map[B](f: A => B): SqlResult[B] に対して、case class ~[A, B](_1: A, _2: B) を部分適用した関数 new ~(a, _) すなわち B => [A ~ B] を渡すことで、Row => SqlResult[A ~ B] に変換している。
trait RowParser[+A] extends (Row => SqlResult[A]) { parent =>
...
def ~[B](p: RowParser[B]): RowParser[A ~ B] =
RowParser(row => parent(row).flatMap(a => p(row).map(new ~(a, _))))
...
}
ケースクラス ~ のパターンマッチによって得るのは、RowParser の match 式ではない。map に PartialFunction を渡して、パースされたカラム値を取り出し、新たな RowParser に変換するのである。
val parser: RowParser[~[~[~[Long, String], Int], java.util.Date]] =
long("id") ~ str("email") ~ int("status") ~ date("birthday")
val userParser: RowParser[User] = parser map {
case ~(~(~(id, email), status), birthday) =>
User(id, email, status, new java.util.Date(birthday.getTime))
}
Row
Row は、java.sql.ResultSet を内部に持つ Cursor を介して得られる。
sealed trait Cursor {
def row: Row
def next: Option[Cursor]
}
object Cursor {
private[anorm] def apply(rs: ResultSet): Option[Cursor] =
if (!rs.next) None else Some(new Cursor {
...
})
...
}
カラム情報 MetaData と SELECT 節の List[Any] を内部に持ち、apply[T] でカラム名か位置番号からカラム値を得ることができる。
trait Row {
private[anorm] def metaData: MetaData
private[anorm] val data: List[Any]
...
def apply[B](name: String)(implicit c: Column[B]): B = ???
def apply[B](position: Int)(implicit c: Column[B]): B = ???
...
}
Column[T] が Any から指定した型への変換器で、anorm.Column._ に implicit で変換可能なパターンが定義されている。
trait Column[A] extends ((Any, MetaDataItem) => MayErr[SqlRequestError, A])
Row.unapplySeq が定義されているので、パターンマッチで SELECT 節を得ることができる。SqlResult への変換を行う必要があるが、カラム値の条件(組み合わせ)によって処理を分けたり、エラーとすることができる。
val parser = RowParser[(Long, Map)] {
case Row(id: Long, email: Some(String)) => Success(id -> email)
case _ => Error(TypeDoesNotMatch("The email must be not null"))
}
val userMap = SQL("SELECT id, email FROM users").as(parser.*).toMap
SqlResult
SqlResult は Row の指定カラムの解析結果となる。
case class Success[A](a: A) extends SqlResult[A]
case class Error(msg: SqlRequestError) extends SqlResult[Nothing]
モナド則を満たしており、連結した RowParser で複数カラムを変換する過程で、いずれかに失敗するとエラーになる。
object SqlParser {
...
def get[T](name: String)(implicit extractor: Column[T]): RowParser[T] =
RowParser { row =>
(for {
// Does the column exist?
col <- row.get(name) // MayErr[SqlRequestError, (Any, MetaDataItem)]
// Can the extractor convert the column value?
res <- extractor.tupled(col) // (Any, MetaDataItem) => MayErr[SqlRequestError, A]
} yield res).fold(Error(_), Success(_))
}
}
MayErr についてはAPI公開されているが、すでに非推奨であり使うことはない。 for-comprehension で記述するための内部クラスで Either の RightProjection のようなものと理解しておけばよい。
ResultSetParser
ResultSetParser[+A] の実体は 関数 Option[Cursor] => SqlResult[A] である。RowParser のメソッドから得られる。
val parser: RowParser[(Long, String)] = long("id") ~ str("email") map {
case id ~ email => (id -> email)
}
// Possibly empty list
val userList: List[(Long, String)] = SQL("SELECT * FROM users").as(parser.*)
// Raise error if there is no result
val notEmptyList: List[(Long, String)] = SQL("SELECT * FROM users").as(parser.+)
// Expecting exactly one row
val user: (Long, String) = SQL("SELECT * FROM users WHERE id = {id}")
.on('id -> 1).as(parser.single)
// Expecting none or one row
val userOpt: Option[(Long, String)] = SQL("SELECT * FROM users WHERE id = {id}")
.on('id -> 2).as(parser.singleOpt)