Locked History Actions

Diff for "Play/Anorm"

Differences between revisions 5 and 6
Deletions are marked like this. Additions are marked like this.
Line 169: Line 169:

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]:
列の値がnullになる可能性があるならば、それをOptionタイプとして取り扱わなければならない。

例えば、CountryテーブルのindepYearがnullableのときには、Option[Short]としてマッチさせる必要がある。
Line 180: Line 179:
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: もし、これをShortとしてマッチさせることにすると、nullの場合をパースすることはできない。
ディクショナリから直接的に列の内容をShortとして取り出そうとすると:
Line 188: Line 188:
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: 値がnullの場合はUnexpectedNullableFound(COUNTRY.INDEPYEAR)という例外が発生する。
だから、適切にOption[Short]にマップさせる必要がある。
Line 196: Line 197:
This rule is also true for the parser API we will just see. 次に見るパーサーAPIでもこのルールが適用される。

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な列を扱う

列の値がnullになる可能性があるならば、それをOptionタイプとして取り扱わなければならない。

例えば、CountryテーブルのindepYearがnullableのときには、Option[Short]としてマッチさせる必要がある。

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

もし、これをShortとしてマッチさせることにすると、nullの場合をパースすることはできない。 ディクショナリから直接的に列の内容をShortとして取り出そうとすると:

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

値がnullの場合はUnexpectedNullableFound(COUNTRY.INDEPYEAR)という例外が発生する。 だから、適切にOption[Short]にマップさせる必要がある。

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

次に見るパーサーAPIでもこのルールが適用される。

パーサコンビネータ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?")