Hatena::ブログ(Diary)

すにぺっと

2011-04-24

play-scalaを改めて学ぶ-11 Anormを使用するその2

| 10:46 | play-scalaを改めて学ぶ-11  Anormを使用するその2を含むブックマーク

Anormの使用方法その2。

ひきつづきworldサンプルDBを使用していろいろ試していく。

http://scala.playframework.org/documentation/scala-0.9/anorm


Using Pattern Matching

また、行の内容についてパターンマッチングができる。

この場合、列の名前は重要ではなく、

パラメータの型と順序を見てマッチングを行う。

次の例では、正しい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.>||

ここの意味わかんね・・・

->

collectに渡されるpartial functionの中にないパターンは無視されるから、予期しないパターンを持つ行は無視されてsafeです。

※コメントありがとうございます


Dealing with Nullable columns

DBスキーマがNullを許容するカラムを含む場合、Opiton型を使うことができる。

例えば、CountryテーブルのindepYearがnull許可の場合、

Option[shor]としてマッチさせる必要がある。

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

Short型として列を一致させようとする場合、Nullを解析することはできない。

直接dictionnaryからShortを取得しようとするとこうなる。

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

もしNull値が発生した場合、UnexpectedNullableFound(COUNTRY.INDEPYEAR)例外が生成される。

だからShortではなく、Option[Short]にマップする必要がある。

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

このルールはパーサーAPIにも当てはまる。


Using the Parser combinator API

ScalaのパーサAPIは、一般的なパーサコンビネータを提供している。

play-scalaは任意のSelectクエリの結果を解析するためにそれらを使用することができる。


まず、play.db.anorm.SqlParser._をインポートする。


使用するパーサーを指定するには、SQL文の(...)メソッドを使用。

たとえば、scalar[Long]はLongとして単一の列の行をパースすることができる単純なパーサー。

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

もっと複雑なパーサーについて考えてみる。

str("name") ~< int("population") *とは、

String型のnameカラム、Int型のpopulationカラムの内容をパースする行ごとに繰り返される。

ここでは、同じ行を読んで、いくつかのパーサを組み合わせるための〜<を使用している。

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

このクエリの結果の型は、そのリストの[String〜Int]、国名と人口の項目のリスト。

シンボルを使用して、同じコードを書き換えることもできる。

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

または、

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

(...)を使ってResultSetを解析する場合、すべての入力を使う必要がある。

パーサーが使用可能なすべての入力を処理しない場合、エラーになる。

入力の一部のみを解析する場合、(...)の代わりにparse(...)使用する。

しかし、エラーの検出が難しくなる可能性があるので、注意して使用すること。

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

今度は、より複雑な例を試してみる。

次のクエリの結果をparse(...)するにはどうするか?

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

このクエリは、joinを使用する。

パーサーは、いくつかの行にまたがる、単一の項目を生成するResultSetが必要。

このパーサを構築するためにspanMコンビネータを使用する。

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

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

最後に、公式言語と他のものを分離するために例を複雑にしてみる。

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

worldサンプルデータベースで試してみると、このように表示される。

$ spokenLanguages("FRA")
> Some(
    SpokenLanguages(France,Some(French),List(
        Arabic, Italian, Portuguese, Spanish, Turkish
    ))
)
ここは難しい。。。
Scalaのパーサーコンビネータのところから
もう一度復習しなおそう。

Adding some Magic[T]

これらの概念に基づいて、playにはパーサーを書くのを助けるMagicヘルパーを提供している。

これはDBのテーブルに一致するテストケースクラスを定義する場合にパーサを生成してくれる。


Magicパーサーは、DBスキーマScala構造にマップするルール規則が必要。

この例では、正確にテーブル名とクラス名を使用してテーブルにScalaのcase classをマップする

デフォルトの規約、およびカラム名のフィールド名を使用する。

まずはimportが必要。

import play.db.anorm.defaults._

クラスを定義してCountryテーブルで試してみる。

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

テストケースクラス内のすべての既存テーブル列を指定する必要がないことに注意。

サブセットのみで十分。

ここで、自動でCountryのパーサを生成する、Magicを拡張したオブジェクトを作成してみる。

object Country extends Magic[Country]

クラス名と別のテーブル名をマッピングしたい場合、指定することもできる。

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

シンプルなCuntry用パーサーを使用できる。

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

Magicは自動的に基本的なSQLクエリを生成することができるメソッドを提供。

val c:Long = Country.count().single()
val c:Long = Country.count("population > 1000000").single()
val c:List[Country] = Country.find().list() //Listだと失敗。Seqじゃね?
val c:List[Country] = Country.find("population > 1000000").list() //Listだと失敗。Seqじゃね?
val c:Option[Country] = Country.find("code = {c}").on("c" -> "FRA").first()

マジックは、update/insertメソッドも提供する。

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

最後に、CityとCountryLanguageケースクラス、そして複雑なクエリを作成してみる。

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


これはすげー便利だ。

単純なクエリMagic使えばいいし、

複雑なSQLが必要なら直書きでもOK。

Scala作法にあわせてOptionとかもうまく処理してくれるし、

パーサー書けば柔軟に対応可能。

play-scalaの魅力がますます上がった。

これを使わない手はないぞ。



プログラミングScala
プログラミングScala
posted with amazlet at 11.04.24
Dean Wampler Alex Payne
オライリージャパン
売り上げランキング: 158001

tmiyamontmiyamon 2011/11/20 22:11 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.>||
collectに渡されるpartial functionの中にないパターンは無視されるから、予期しないパターンを持つ行は無視されてsafeですね、という話ですかね

トラックバック - http://d.hatena.ne.jp/sy-2010/20110424/1303609562