Locked History Actions

Diff for "Play/Anorm"

Differences between revisions 4 and 5
Deletions are marked like this. Additions are marked like this.
Line 105: Line 105:
Another variant is to fill them by position: 「位置」によって埋めることもできる(訳注:{}が出てきた順番に値を並べる?)
Line 117: Line 117:
Retrieving data using the Stream API

The first way to access data coming from a Select query, is to use the Stream API.

When you call apply() on any SQL statement, you will receive a lazy Stream of Row, where each row can be seen as a dictionary:

// Create an SQL query
=== Stream APIを使ってデータを取得する ===

selectクエリによって得られたデータにアクセスする最初の方法としてはStream APIによるものがある。

SQLステートメントのapply()を呼び出すと、行のlazyなストリームを得ることができる。
これにより、各行をディクショナリとして扱うことができる。

{{{
// SQLクエリを作成する
Line 126: Line 128:
// Transform the resulting Stream[Row] as a List[(String,String)] // 結果のStream[Row]List[(String,String)]に変換する。
Line 130: Line 132:

In the following example we will count the number of Country in the database. So the resultSet will be a single row with a single column:

// First retrieve the first row
}}}

次の例では、データベース中のCountryの数をカウントするので、resultsetは単一行の単一列になる。

{{{
// 最初に、最初の行を取り出す。
Line 136: Line 140:
// Next get the content of the 'c' column as Long // 次に、c列の内容をLongとして取り出す。
Line 138: Line 142:

Using Pattern Matching

You can also use Pattern Matching to match and extract the Row content. In this case the column name doesn’t matter. Only the order and the type of the parameters is used to match.

The following example transform each row to the correct Scala type:
}}}

=== パターンマッチングを使う ===

行内容の取り出しにパターンマッチングを使うこともできる。
この場合、列の名称はどうでもよい。
順序とタイプのみがマッチに使用される。


次の例では、各行を正しいScalaタイプに変換している。

{{{
Line 154: Line 163:
}}}
Line 156: Line 166:
Dealing with Nullable columns
=== Nullableな列を扱う ===
Line 162: Line 174:
{{{
Line 165: Line 178:
}}}
Line 168: Line 182:
{{{
Line 171: Line 186:
}}}
Line 174: Line 190:
{{{
Line 177: Line 194:
}}}
Line 179: Line 197:
Using the Parser combinator API
=== パーサコンビネータAPIを使う ===
Line 187: Line 206:
{{{
Line 188: Line 208:
}}}
Line 193: Line 214:
{{{
Line 196: Line 218:
}}}
Line 201: Line 224:
{{{
Line 204: Line 228:
}}}
Line 207: Line 232:
{{{
Line 212: Line 238:
}}}
Line 217: Line 244:
{{{
Line 222: Line 250:
}}}
Line 225: Line 254:
{{{
Line 228: Line 258:
}}}
Line 231: Line 262:
{{{
Line 232: Line 264:
}}}
Line 235: Line 268:
{{{
Line 253: Line 287:
}}}
Line 256: Line 291:
{{{
Line 283: Line 319:
}}}
Line 286: Line 323:
{{{
Line 292: Line 330:

Adding some Magic[T]
}}}

=== Magic[T]を追加する ===
Line 301: Line 340:

{{{
Line 302: Line 343:
}}}
Line 305: Line 347:
{{{
Line 308: Line 351:
}}}
Line 313: Line 357:
{{{
Line 314: Line 359:
}}}
Line 317: Line 363:
{{{
Line 318: Line 365:
}}}
Line 321: Line 369:
{{{
Line 322: Line 371:
}}}
Line 325: Line 375:
{{{
Line 330: Line 381:
}}}
Line 333: Line 385:
{{{
Line 334: Line 387:
}}}
Line 337: Line 391:
{{{
Line 371: Line 426:
}}}

Anormについて

Anorm, SQL data access with Play Scala

Anorm, SQL data access with Play Scalaの抄訳(2011/5, バージョン0.9)

ScalaモジュールにはAnormという新しいDBアクセスが含まれる。これには、生のSQLを使って問い合わせを行い、結果データセットをパース・変換するためのAPIがある。

AnormはORMではない

以下ではMySQLをサンプルとして説明するので、実行してみたい場合はMySQLウェブサイトの手順に従い、 以下の設定をconf/application.confファイルに記述して欲しい。

db=mysql:root@world

概要

今日のSQLデータベースアクセスにおいて、生の古いSQLに戻るというのは妙に感じされるかもしれない。 特にJavaデベロッパはHibernateのような高レベルなORMを使ってのアクセスになれており、生SQLは完全に隠されていると思われる。

このようなツールがJavaでは必要であることに賛成するとしても、我々はScalaのような高レベル言語では全く必要が無いと考えているし、 逆に生産性を落とすと考える。

JDBCを扱うのは苦痛だ。しかし、我々はもっと良いAPIを提供する。

JDBC APIはめんどくさい。特にJavaでは。あちこちで例外をチェックしなければいけないし、ResultSetを自分のデータ構造に変換するのに、何度も何度も繰り返しを行わなければならない。

これに対して我々は、JDBC向けの単純なAPIを提供することにした。SCalaを使えば例外に邪魔されることもないし、関数型言語によってデータ変換も非常に簡単になる。 Play ScalaはJDBCデータをScalaの構造に簡単に変換するアクセスレイヤーを提供している。

RDBにアクセスするための別のDSLは必要ない

SQLはRDBにアクセスするためのベストなDSLなのである。別の発明をする必要などない。 さらに、データベースベンダによってSQLの構文は異なっているのである。

これを別のDSLに置き換えようとすれば、ベンダーによる様々な「方言」を扱わなければならない(Hibernateのように)。 さらに、データベースの提供する特別な機能を使わないように制限しなければならない。

我々はprefilledなSQLステートメントを提供することもあるが、しかし我々が水面下でSQLを使用していることを隠すものではない。 単にちょっとしたクエリ文字列をタイプしなくても済むようにしているだけであって、いつでもプレーンな古いSQLにアクセスすることができる。

SQL生成のためのタイプセーフなDSLは「間違い」

タイプセーフなDSLはコンパイラによってクエリの間違いをチェックできるから良いのだ、という人もいる。 残念なことに、コンパイラは、データ構造とRDBの「マッピング」によるメタモデル定義に基づいてチェックしていうだけである。

メタモデルの正当性の保証はない。クエリが完全であることをコンパイラが保証したとしても、実際のRDBの定義とは異なれば実行には失敗してしまう。

SQLコードを制御する

ORMがうまくいくケースもある。が、既存の複雑なスキーマを持つRDBを扱うなら、所望のSQLクエリを生成させるためには、ORMと格闘しなければならない。

簡単な「Hello World」アプリのためにSQLを記述するのは面倒だが、現実のアプリでSQLコードを記述することは、最終的には時間の節約になるし、コードもシンプルになる。

さてに、Play ScalaでSQLデータベースをどのように扱うのかを見ていこう。

SQLリクエストを実行する

まずはSQLリクエストの実行の仕方を学ぶ必要がある。

まずは、play.db.anorm._をインポートし、クエリ作成のためにSQLオブジェクトを使う。

import play.db.anorm._ 
 
val result:Boolean = SQL("Select 1").execute()

execute()メソッドが成功したか否かがBoolean値で返される。 更新クエリではexecuteUpdate()を使う。これはMayErr[IntegrityConstraintViolation,Int]値を返す。

val result = SQL("delete from City where id = 99").executeUpdate().fold( 
    e => "Oops, there was an error" , 
    c => c + " rows were updated!"
)

Scalaは複数行文字列をサポートしているので、複雑なSQLステートメントは次のように記述してもよい。

var sqlQuery = SQL(
    """
        select * from Country c 
        join CountryLanguage l on l.CountryCode = c.Code 
        where c.code = 'FRA';
    """
)

パラメータを与えたければ、クエリ文字列中にプレースホルタとして{name}のように記述し、後にその値を代入することができる。

SQL(
    """
        select * from Country c 
        join CountryLanguage l on l.CountryCode = c.Code 
        where c.code = {countryCode};
    """
).on("countryCode" -> "FRA")

「位置」によって埋めることもできる(訳注:{}が出てきた順番に値を並べる?)

SQL(
    """
        select * from Country c 
        join CountryLanguage l on l.CountryCode = c.Code 
        where c.code = {countryCode};
    """
).onParams("FRA")

Stream APIを使ってデータを取得する

selectクエリによって得られたデータにアクセスする最初の方法としてはStream APIによるものがある。

SQLステートメントのapply()を呼び出すと、行のlazyなストリームを得ることができる。 これにより、各行をディクショナリとして扱うことができる。

// SQLクエリを作成する
val selectCountries = SQL("Select * from Country")
 
// 結果のStream[Row]をList[(String,String)]に変換する。
val countries = selectCountries().map(row => 
    row[String]("code") -> row[String]("name")
).toList

次の例では、データベース中のCountryの数をカウントするので、resultsetは単一行の単一列になる。

// 最初に、最初の行を取り出す。
val firstRow = SQL("Select count(*) as c from Country").apply().head
 
// 次に、c列の内容をLongとして取り出す。
val countryCount = firstRow[Long]("c")

パターンマッチングを使う

行内容の取り出しにパターンマッチングを使うこともできる。 この場合、列の名称はどうでもよい。 順序とタイプのみがマッチに使用される。

次の例では、各行を正しい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)      
}

Note that since collect(…) ignore the cases where the partial function isn’t defined, it allow your code to safely ignore rows that you don’t expect.

Nullableな列を扱う

If a column can contain Null values in the database schema, you need to manipulate it as an Option type.

For example, the indepYear of the Country table being nullable, you need to match it as Option[Short]:

SQL("Select name,indepYear from Country")().collect {
    case Row(name:String, Some(year:Short)) => name -> year
}

If you try to match this column as Short it won’t be able to parse Null cases. If you try to retrieve the column content as Short directly from the dictionnary:

SQL("Select name,indepYear from Country")().map { row =>
    row[String]("name") -> row[Short]("indepYear")
}

It will produce an UnexpectedNullableFound(COUNTRY.INDEPYEAR) exception if it encounter a null value. So you need to map it properly to an Option[Short], as:

SQL("Select name,indepYear from Country")().map { row =>
    row[String]("name") -> row[Option[Short]]("indepYear")
}

This rule is also true for the parser API we will just see.

パーサコンビネータAPIを使う

The Scala Parsers API provides generic parser combinators. Play Scala can use them to parse the result of any Select query.

First you need to import play.db.anorm.SqlParser._.

Use the as(…) method of the SQL statement to specify the parser you want to use. For example scalar[Long] is a simple parser that knows how to parse a single column row as Long:

val count:Long = SQL("select count(*) from Country").as(scalar[Long])

Let’s write a more complicated parser:

str("name") ~< int("population") *, will parse the content of the name column as String, then the content of the population column as Int, and will repeat for each row. Here we use ~< to combine several parsers that read the same row.

val populations:List[String~Int] = {
    SQL("select * from Country").as( str("name") ~< int("population") * ) 
}

As you see, the result type of this query is a List[String~Int], so a list of country name and population items.

You can also, use Symbol and rewrite the same code as:

val populations:List[String~Int] = {
    SQL("select * from Country").as('name.of[String]~<'population.of[Int]*) 
}

Or even as:

val populations:List[String~Int] = {
    SQL("select * from Country").as( 
        get[String]("name") ~< get[Int]("population") *
    ) 
}

When you parse a ResultSet using as(…) it must consume all the input. If your parser doesn’t consume all the available input, an error will be thrown. It avoids to have your parser fails silently.

If you want to parse only a small part of the input, you can use parse(…) instead of as(…). However use it with caution, as it make it more difficult to detect errors in your code:

val onePopulation:String~Int = {
    SQL("select * from Country").parse( 
        str("name") ~< int("population")
    )
}

Now let’s try with a more complicated example. How to parse the result of the following query?

select c.name, c.code, l.language from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = 'FRA'

As this query uses a join, our parser will need to span several rows of the ResultSet to generate a single item. We will use the spanM combinator to construct this parser:

str("name") ~< spanM(by=str("code"), str("language"))

Now let’s use this parser to create a function that gives us all languages spoken in a country:

case class SpokenLanguages(country:String, languages:Seq[String])
 
def spokenLanguages(countryCode:String):Option[SpokenLanguages] = {
    SQL(
        """
            select c.name, c.code, l.language from Country c 
            join CountryLanguage l on l.CountryCode = c.Code 
            where c.code = {code};
        """
    )
    .on("code" -> countryCode)
    .as(
        str("name") ~< spanM(by=str("code"), str("language")) ^^ { 
            case country~languages => SpokenLanguages(country, languages)
        } ?
    )
    
}

Finally, let’s complicate our example to separate the official language and the other ones:

case class SpokenLanguages(
    country:String, 
    officialLanguage: Option[String], 
    otherLanguages:Seq[String]
)
 
def spokenLanguages(countryCode:String):Option[SpokenLanguages] = {
    SQL(
        """
            select * from Country c 
            join CountryLanguage l on l.CountryCode = c.Code 
            where c.code = 'FRA';
        """
    ).as(
        str("name") ~< spanM(
            by=str("code"), str("language") ~< str("isOfficial") 
        ) ^^ { 
            case country~languages => 
                SpokenLanguages(
                    country,
                    languages.collect { case lang~"T" => lang } headOption,
                    languages.collect { case lang~"F" => lang }
                )
        } ?
    )
    
}

If you try this on the world sample database, you will get:

$ spokenLanguages("FRA")
> Some(
    SpokenLanguages(France,Some(French),List(
        Arabic, Italian, Portuguese, Spanish, Turkish
    ))
)

Magic[T]を追加する

Based on all these concepts, Play provides a Magic helper that will help you to write parsers. The idea is that if you define a case class that match a database table, Play Scala will generate a parser for you.

The Magic parsers need a convention to map you Scala structures to your database scheme. In this example we will use the default convention that map Scala case classes to Tables using exactly the class names as table name, and the field names as column names.

So before continuing, you need to import:

import play.db.anorm.defaults._

Let’s try by defining a first Country case class that describes the Country table:

case class Country(
    code:Id[String], name:String, population:Int, headOfState:Option[String]
)

Note that we are not required to specify every existing table column in the case class. Only a subset is enough.

Now let’s create an object that extends Magic to automatically get a parser of Country:

object Country extends Magic[Country]

If you want to break the convention here and use a different table name to for the Country case class, you can specify it:

object Country extends Magic[Country]().using("Countries")

And we can simply use Country as Country parser:

val countries:List[Country] = SQL("select * from Country").as(Country*)

Magic provides automatically a set of methods that can generate basic SQL queries:

val c:Long = Country.count().single()
val c:Long = Country.count("population > 1000000").single()
val c:List[Country] = Country.find().list()
val c:List[Country] = Country.find("population > 1000000").list()
val c:Option[Country] = Country.find("code = {c}").on("c" -> "FRA").first()

Magic also provides the update and insert methods. For example:

Country.update(Country(Id("FRA"), "France", 59225700, Some("Nicolas S.")))

Finally, let’s write the missing City and CountryLanguage case classes, and make a more complex query:

case class Country(
    code:Id[String], name:String, population:Int, headOfState:Option[String]
)
 
case class City(
    id:Pk[Int], name: String
)
 
case class CountryLanguage(
    language:String, isOfficial:String
)
 
object Country extends Magic[Country]
object CountryLanguage extends Magic[CountryLanguage]
object City extends Magic[City]
 
val Some(country~languages~capital) = SQL(
    """
        select * from Country c 
        join CountryLanguage l on l.CountryCode = c.Code 
        join City v on v.id = c.capital 
        where c.code = {code}
    """
)
.on("code" -> "FRA")
.as( Country.span( CountryLanguage * ) ~< City ? )
 
val countryName = country.name
val capitalName = capital.name
val headOfState = country.headOfState.getOrElse("No one?")
 
val officialLanguage = languages.collect { 
                           case CountryLanguage(lang, "T") => lang 
                       }.headOption.getOrElse("No language?")