§Anorm によるシンプルな SQL データアクセス
Play には Anorm と呼ばれるシンプルなデータアクセスレイヤーが同梱されています。Anorm はデータベースとやり取りするのに SQL をそのまま利用すると同時に、結果のデータセットをパースしたり変換するための API を提供します。
Anorm は ORM (Object Relational Mapper) ではない
以下のドキュメントでは、MySQL world サンプルデータベース を利用します。
もし、ご自分のアプリケーションでこのデータセットを利用されたい場合は、 MySQL ウェブサイトの手順の通りに従った後に、 Scala database のページ で説明しているように設定してください。
§概観
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 のように) ベンダ固有の dialects
に対応する必要があります。これは同時に、データベースの特有の便利な機能の利用を自ら制限することにもなってしまいます。
Play には SQL ステートメントの組み立てを補助する機能も提供しています。しかし、その主な目的は SQL を隠蔽することではありません。Play は平凡なクエリについてタイプ量を削減してくれるだけであり、いつでも素の SQL に戻ることができます。
§SQL を生成するための型安全な DSL は間違い
型安全な DSL はコンパイラによって検査されるため、素の SQL より優れていると言われることがあります。しかし、残念ながらコンパイラはあなたが自分自身で書いたデータ構造とデータベーススキーマをマッピングするメタモデル定義をもとにクエリを検査します。
このメタモデルが正しいという保証はありません。コンパイラがあなたのコードやクエリが正しくタイプされていると判断したとしても、メタモデルと実際のデータベーススキーマにミスマッチがあれば、残念なことに実行時エラーになってしまいます。
§SQL コードをコントロールする
Object Relational Mapping は平凡なケースではうまくいきます。しかし、実際にあなたが直面すると予想される複雑なスキーマや既存のデータベースに対応する必要があるとき、あなたが求める SQL クエリを生成するために ORM との格闘に多くの時間を費やすことになるでしょう。
SQL クエリを自分で書くのはシンプルな Hello World
アプリケーションのような場合にはつまらないものに感じられますが、実際のアプリケーションでは SQL コードを完全にコントロールできることが結果的に時間を節約し、あなたのコードを簡潔にするでしょう。
§プロジェクトに Anorm を追加する
依存ライブラリに Anorm と jdbc プラグインを追加する必要があります :
val appDependencies = Seq(
jdbc,
anorm
)
§SQL クエリを実行する
まずは SQL の実行方法を知る必要があります。
初めに、 anorm._
をインポートして、 SQL
オブジェクトを使ってクエリを作成しましょう。クエリを実行するためには Connection
が必要なので play.api.db.DB
ヘルパーを使います。
import anorm._
import play.api.db.DB
DB.withConnection { implicit c =>
val result: Boolean = SQL("Select 1").execute()
}
execute()
メソッドは SQL 実行が成功したかどうかを表す Boolean 値を返します。
Update を実行するためには、executeUpdate()
が利用できます。このメソッドは更新された行数を返します。
val result: Int = SQL("delete from City where id = 99").executeUpdate()
自動採番の Long
型の主キーを持つデータを挿入する場合は、executeInsert()
を使います。生成されるキーが一つより多い場合や Long 型でない場合、正しいキーを返却する ResultSetParser
を executeInsert
に渡します。
val id: Option[Long] = SQL("insert into City(name, country) values ({name}, {country})")
.on('name -> "Cambridge", 'country -> "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
を取得できます。各行(Row
)はディクショナリのデータ型になっています。
// 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
の行数を数えます。結果セットは一つのカラムを持つ一行だけの結果になります。
// 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
CLOB/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
}
§データベースの相互運用
異なるデータベースからは同じ行でも異なるデータ型が返ることに注意してください。例えば、 SQL ‘smallint’ は org.h2.Driver から Short 型として返され、 org.postgresql.Driver からは Integer 型として返されます。これに対する一つの解決策は、単に各データベース(例:開発用と本番用)に対して case 文を分けることです。
§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
値をパースできません。辞書型の row から カラムの内容を 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._
を書きます。
§結果を1行取得する
まずは RowParser
が必要です。これは、一行をパースして Scala の値へ変換するパーサーです。例えば、一カラムの結果セットの行をパースして Scala の Long
値を生成するようなパーサーは次のように記述します。
val rowParser = scalar[Long]
次に、これを ResultSetParser
へ変換する必要があります。次のように、一行をパースするパーサを定義しましょう。
val rsParser = scalar[Long].single
このパーサーは結果セットをパースして Long
値を返します。これは、select count
のような単純な SQL 文の結果をパースするような場合に便利です。
val count: Long = SQL("select count(*) from Country").as(scalar[Long].single)
§Option型の結果を1行取得する
coutry name から country_id を読み出す場合を考えてみましょう。しかし、クエリはnullを返すかもしれません。 その場合は singleOpt パーサーを使いましょう:
val countryId: Option[Long] = SQL("select country_id from Country C where C.country='France'").as(scalar[Long].singleOpt)
§より複雑な結果を取得する
もっと複雑なパーサーを書いてみましょう。
str("name") ~ int("population")
は String 型 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)
)
}
}
これを MySQL での world サンプルデータベースに対して実行すると、次のような結果が得られます。
$ spokenLanguages("FRA")
> Some(
SpokenLanguages(France,Some(French),List(
Arabic, Italian, Portuguese, Spanish, Turkish
))
)
次ページ: その他のデータベースライブラリとの統合
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。