§Anorm によるシンプルなデータアクセス
Play には Anorm と呼ばれるシンプルなデータアクセスレイヤーが同梱されています。Anorm はデータベースとやり取りするのに SQL をそのまま利用すると同時に、結果のデータセットをパースしたり変換するための API を提供します。
Anorm は ORM (Object Relational Mapper) ではありません
以下のドキュメントでは、MySQL world サンプルデータベース を利用します。
もし、ご自分のアプリケーションでこのデータセットを利用されたい場合は、 MySQL ウェブサイトの手順の通りに従った後、次の数行を `conf/application.conf に記述して、データベースを有効にしてください。
db.default.driver= com.mysql.jdbc.Driver db.default.url="jdbc:mysql://localhost/world" db.default.user=root db.default.password=secret
§概観
Hibernate のような、SQL を完全に隠蔽するような高級な ORM に慣れた Java デベロッパーの方にとって、Anorm がデータベースアクセスに昔ながらの SQL を直接使っていることが奇妙に思われるかもしれません。
そのようなツールは、確かに Java では必要だったと思います。しかしながら,Scala のような高級な言語の恩恵を受けられる場合には全く必要ではないと考えます。実際のところ、そのような高級な ORM は Scala の場合はすぐ非生産的に感じられてしまうでしょう。
§JDBC より良い API
特に Java で JDBC API を直接使うことには飽き飽きされていると思います。Java で JDBC を直接使うような場合、コードのあらゆる箇所でチェック例外を考慮しなければならず、また生のデータセットをアプリケーション独自のデータ構造に変換するために ResultSet を何度も何度も読み込む必要があります。
そこで、私たちは JDBC よりシンプルな API を作ることにしました。Scala を利用すると、チェック例外に邪魔されることなく、また関数型言語の機能によりデータ構造の変換が本当に簡単になります。実際、Play の Scala SQL アクセスレイヤーが提供しているのは、JDBC のデータを Scala で定義されたデータ構造へ効率的に変換するための API です。
§リレーショナルデータベースへアクセスするために新しい DSL は必要ありません
SQL はそれ自体が既にリレーショナルデータベースへアクセスするために最適な DSL です。したがって、SQL に代わる何か新しいものを発明する必要はありません。また、SQL の文法や機能はデータベースベンダーによって異っています。
このような SQL の方言をプロプライエタリかつ SQL ライクな DSL で抽象化しようとすると、(Hibernate のそれのように) ベンダ固有の dialeects
を利用する必要が出てしまいます。これは同時に、データベースの特有の便利な機能を自ら制限することにもなってしまいます。
Play には SQL ステートメントの組み立てを補助する機能もあります。しかし、その主な目的は SQL を隠蔽してしまうことではありません。Play は平凡なクエリについてタイプ量を削減してくれるだけであり、必要であればいつもどおりの生の SQL へフォールバックすることができます。
§SQL を生成するための型安全な DSL は間違い
型安全な DSL は、クエリがコンパイラによって検証されるという点において、ただの SQL より良いと言われることがあります。ただ残念なことに、このようなコンパイラのチェックは、あなたがデータ構造をデータベースのスキーマに人力でマッピングして作ったメタモデルに対するクエリに対して行われます。
このとき、定義したメタモデルが正しいという保証はありません。コンパイラがあなたのコードやクエリが正しく打ち込まれていると判断したとしても、メタモデルとデータベースのスキーマにミスマッチがあれば、残念なことに実行時エラーになってしまいます。
§SQL コードをコントロールする
Object Relational Mapping は一般的なデータモデルに関してはうまくいきます。しかし、実際にあなたが直面すると予想される複雑なスキーマや既存のデータベースに対しては、ORM を使って SQL クエリーを生成するのはかなりの負担になります。
SQL クエリを自分で書くのは Hello World
アプリケーションのようなシンプルな場合には面倒ですが、実際のアプリケーションでは結果的に時間の節約やコードのシンプル化につながります。
§SQL クエリを実行する
まずは SQL の実行方法を知る必要があります。
初めに、 anorm._
をインポートして、 SQL
オブジェクトを使ってクエリを作成しましょう。クエリを実行するためには Connection
が必要で、その習得には play.api.db.DB
ヘルパー関数が利用できます。
import anorm._
DB.withConnection { implicit c =>
val result: Boolean = SQL("Select 1").execute()
}
execute()
メソッドは実行が成功したかどうかを表す Boolean 値を返します。
Update を実行するためには、executeUpdate()
が利用できます。この関数はアップデートされた行数を返します。
val result: Int = SQL("delete from City where id = 99").executeUpdate()
自動生成された Long
型の主キーを持つデータを Insert するのであれば、executeInsert()
を呼び出すことができます。生成されるキーがひとつより多い場合や Long 型でない場合、正しいキーを返却する ResultSetParser
を executeInsert
に渡すことができます。
val id: Int = SQL("insert into City(name, country) values ({name}, {country}")
.on("Cambridge", "New Zealand").executeInsert()
Scala は複数行の文字列リテラルをサポートしているため、複雑な SQL ステートも気軽に書けます。
val sqlQuery = SQL(
"""
select * from Country c
join CountryLanguage l on l.CountryCode = c.Code
where c.code = 'FRA';
"""
)
もし SQL クエリが動的なパラメータをとるような場合、{name}
のようなプレースホルダをクエリ文字列内に宣言して、後でそこに値を埋め込むことができます。
SQL(
"""
select * from Country c
join CountryLanguage l on l.CountryCode = c.Code
where c.code = {countryCode};
"""
).on("countryCode" -> "FRA")
§Stream API を利用してデータを取得する
select クエリから結果を参照する第一の方法は、 Stream API を利用することです。
SQL 文に対して apply()
を呼び出すと、Row
インスタンスの Stream
を、遅延評価される形で取得することができます。各行は辞書のような構造になっています。
// Create an SQL query
val selectCountries = SQL("Select * from Country")
// Transform the resulting Stream[Row] to a List[(String,String)]
val countries = selectCountries().map(row =>
row[String]("code") -> row[String]("name")
).toList
次の例ではデータベース内の Country
の個数を数えます。ResultSet は一カラム・一行になります。
// First retrieve the first row
val firstRow = SQL("Select count(*) as c from Country").apply().head
// Next get the content of the 'c' column as Long
val countryCount = firstRow[Long]("c")
§パターンマッチの利用
Row
の内容を抽出するためにパターンマッチを利用することができます。このケースでは、カラム名は関係ありません。パラメータの順番と型だけがパターンマッチの際に考慮されます。
次の例は各行を適切な Scala の型に変換します。
case class SmallCountry(name:String)
case class BigCountry(name:String)
case class France
val countries = SQL("Select name,population from Country")().collect {
case Row("France", _) => France()
case Row(name:String, pop:Int) if(pop > 1000000) => BigCountry(name)
case Row(name:String, _) => SmallCountry(name)
}
collect(…)
は部分関数が定義されていない case を無視してくれるので、期待していない行を安全に読み飛ばすことができます。
§特別なデータ型
§Clobs
CLOBs/TEXT は以下のようにして取り出すことができます:
SQL("Select name,summary from Country")().map {
case Row(name: String, summary: java.sql.Clob) => name -> summary
}
行が期待するフォーマットでない場合を例外としたいので、ここでは特に map
を使用しています。
§Binary
同様にバイナリデータも取り出すことができます:
SQL("Select name,image from Country")().map {
case Row(name: String, image: Array[Byte]) => name -> image
}
§nullable なカラムを扱う
カラムが データベーススキーマにおいて Null
値を含む場合、Option
型で操作する必要があります。
例えば、Country
テーブルの indepYear
というカラムが nullable の場合、それを Option[Int]
にマッチさせます。
SQL("Select name,indepYear from Country")().collect {
case Row(name:String, Some(year:Int)) => name -> year
}
このカラムを Int
としてマッチさせようとすると、Null
値をパースできません。辞書から カラムの内容を Int
として取得する場合を考えてみましょう。
SQL("Select name,indepYear from Country")().map { row =>
row[String]("name") -> row[Int]("indepYear")
}
このコードは、null 値にヒットした場合に UnexpectedNullableFound(COUNTRY.INDEPYEAR)
という例外を投げます。正しくは次のように Option[Int]
にマップさせる必要があります。
SQL("Select name,indepYear from Country")().map { row =>
row[String]("name") -> row[Option[Int]]("indepYear")
}
これは次で説明する parser API についても同様です。
§Parser API の利用
様々な select 文の結果をパースする汎用的かつ再利用可能なパーサーを定義するためには parser API が利用できます。
Note: Web アプリケーションのほとんどのクエリは同じようなデータセットを返すため、Parser API が効果的です。例えば、result set から
Country
をパースするパーサーと、Language
をパースするパーサーを定義すると、それらを組み合わせ join クエリから Country と Language を同時にパースするパーサーを作成することができます。はじめに、
import anorm.SqlParser._
を書きます。
まずは RowParser
が必要です。これは、一行をパースして Scala の値へ変換するパーサーです。例えば、一カラムの result set の行をパースして Scala の Long
値を生成するようなパーサーは次のように記述します。
val rowParser = scalar[Long]
次に、これを ResultSetParser
へ変換する必要があります。次のように、一行をパースするパーサを定義しましょう。
val rsParser = scalar[Long].single
このパーサーは result set をパースして Long
値を返します。これは、select count
のような単純な SQL 文の結果をパースするような場合に便利です。
val count: Long = SQL("select count(*) from Country").as(scalar[Long].single)
もっと複雑なパーサーを書いてみましょう。
str("name") ~ int("population")
は、文字列型の name
列と、Integer 型の population
列を含む行をパースできる RowParser
を作ります。それから *
を使って、複数のこのような行をパースする ResultSetParser
を作ることができます:
val populations:List[String~Int] = {
SQL("select * from Country").as( str("name") ~ int("population") * )
}
ご覧のとおり、このクエリの結果の型は List[String~Int]
、つまり国名と人口のペアを要素とするリストになります。
このコードは次のように書くこともできます。
val result:List[String~Int] = {
SQL("select * from Country").as(get[String]("name")~get[Int]("population")*)
}
さて、String~Int
という型は一体何でしょうか?これは Anorm で定義されている型で、データベースアクセスに関するコード以外での利用には適していません。例えば、(String, Int)
のような単純なタプルをパースしたいことが多いでしょう。そのためには、 RowParser
の map
関数を使って、結果をもっと便利な型に変換します。
str("name") ~ int("population") map { case n~p => (n,p) }
Note: この例では
(String,Int)
というタプルを生成しましたが、RowParser
の結果をもっと別の型に変換しても何ら問題ありません。例えば、何らかの case class に変換してもよいでしょう。
さて、A~B~C
を (A,B,C)
に変換するというよくあるタスクを簡単にするために、 flatten
という関数を用意しました。これを使うと、同じ例は次のように書けます。
val result:List[(String,Int)] = {
SQL("select * from Country").as(
str("name") ~ int("population") map(flatten) *
)
}
次はさらに複雑な例に挑戦してみましょう。次のクエリの結果から国名と、国コード別の言語を取得するにはどうしたらよいでしょうか?
select c.name, l.language from Country c
join CountryLanguage l on l.CountryCode = c.Code
where c.code = 'FRA'
まず、全ての行から List[(String,String)]
、つまり国名と言語のタプルを要素とするリストをパースすることから始めます。
var p: ResultSetParser[List[(String,String)] = {
str("name") ~ str("language") map(flatten) *
}
結果は以下のようになります。
List(
("France", "Arabic"),
("France", "French"),
("France", "Italian"),
("France", "Portuguese"),
("France", "Spanish"),
("France", "Turkish")
)
次に、この結果を変換して必要なデータを得るために、 Scala の Collection API を利用することができます。
case class SpokenLanguages(country:String, languages:Seq[String])
languages.headOption.map { f =>
SpokenLanguages(f._1, languages.map(_._2))
}
最終的に、次のような便利な関数を作ることができます。
case class SpokenLanguages(country:String, languages:Seq[String])
def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
val languages: List[(String, String)] = SQL(
"""
select c.name, l.language from Country c
join CountryLanguage l on l.CountryCode = c.Code
where c.code = {code};
"""
)
.on("code" -> countryCode)
.as(str("name") ~ str("language") map(flatten) *)
languages.headOption.map { f =>
SpokenLanguages(f._1, languages.map(_._2))
}
}
続いて、もう少し複雑な例として、公式サポートされている言語をそれ以外の言語と分けてみましょう。
case class SpokenLanguages(
country:String,
officialLanguage: Option[String],
otherLanguages:Seq[String]
)
def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
val languages: List[(String, String, Boolean)] = SQL(
"""
select * from Country c
join CountryLanguage l on l.CountryCode = c.Code
where c.code = {code};
"""
)
.on("code" -> countryCode)
.as {
str("name") ~ str("language") ~ str("isOfficial") map {
case n~l~"T" => (n,l,true)
case n~l~"F" => (n,l,false)
} *
}
languages.headOption.map { f =>
SpokenLanguages(
f._1,
languages.find(_._3).map(_._2),
languages.filterNot(_._3).map(_._2)
)
}
}
これを world サンプルデータベースに対して実行すると、次のような結果が得られます。
$ spokenLanguages("FRA")
> Some(
SpokenLanguages(France,Some(French),List(
Arabic, Italian, Portuguese, Spanish, Turkish
))
)
次ページ: データベースアクセスライブラリの利用