Documentation

You are viewing the documentation for the 2.0.x release series. The latest stable release series is 2.4.x.

§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 型でない場合、正しいキーを返却する ResultSetParserexecuteInsert に渡すことができます。

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) のような単純なタプルをパースしたいことが多いでしょう。そのためには、 RowParsermap 関数を使って、結果をもっと便利な型に変換します。

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

次ページ: データベースアクセスライブラリの利用


このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。