You are not allowed to use this action.

Clear message
Locked History Actions

Play/Anorm

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はコンパイラによってクエリの間違いをチェックできるから良いのだ」という人もいる(訳注:SQLは、コンパイラにとっては単なる文字列である)。 しかし残念なことに、コンパイラは、データ構造とRDBの「マッピング」によるメタモデル定義に基づいてチェックしているだけである。

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

SQLコードを制御する

ORMがうまくいくケースもある。が、既存の複雑なスキーマを持つRDBを扱うなら、所望のSQLクエリを生成させるためには、ORMと格闘しなければならない(訳注:実際大変な作業だと思う)。

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

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

SQLリクエストを実行する

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

play.db.anorm._をインポートし、クエリ作成のためにSQLオブジェクトを使う(訳注:「select 1」って何?;)

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

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

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を使う

ScalaのパーサーAPIはジェネリックなコンビネータを提供している。 Play Scalaはselectクエリの結果をパースするためにそれを使うことができる。

最初に「play.db.anorm.SqlParser._」をインポートする。

SQLステートメントのas(...)メソッドを使うことにより、使いたいパーサーを指定する。 例えば、scalar[Long]は、単一列の行をLongとしてパースする方法を知っている単純なパーサーである。

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

もう少し複雑なパーサを書いてみよう。

「str("name") ~< int("population") *」はname列の値をStringとしてパースし、population列の値をIntとする。 これを各行について繰り返す。 ここで我々は同じ行を読む複数のパーサを結合するために~<を使っている。

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

このクエリの結果タイプはa List[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") *
    ) 
}

as(...)を使ってResultSetをパースすると、それはすべての入力を消費する。 もし、パーサがすべての入力を消費しない場合はエラーが発生sる。 このため、何も言わずにパーサが失敗することはない。

入力の一部のみをパースしたい場合は、as(...)の代わりにparse(...)を使うこと。 ただし、注意して欲しいが、エラーを検出することは難しくなる。

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

さて、もう少し複雑な例を示そう。次のクエリの結果はどのようにパースすべきだろうか?

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

このパーサを使って、一つの国で使われるすべての言語を返す関数を作成してみよう。

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

これをサンプルデータベースに適用すると、次のようになる。

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

Magic[T]を追加する

(注:現在のバージョンscala-0.9.1ではPostgreSQLに対してはMagicは使用できない。 Play/AnormPostgreSQLを参照のこと)

以上をベースとした、Playはパーサを記述するためのMagicヘルパを提供している。 アイデアとしてはこうだ。データベーステーブルに合致するcaseクラスを記述すればPlay Scalaはそのパーサを自動で生成する。

MagicパーサはデータベーススキームにScala構造をマップするconvention(訳注:Convention over configurationのconvention)を必要とする。 以下の例では、デフォルトのconventionを使う。 つまり、Scalaのcaseクラスをテーブルにマップするのに、クラス名がテーブル名に一致していることとフィールド名が列名に一致していることが必要である。

始める前にimportしておこう。

import play.db.anorm.defaults._

訳注:これに加えて当然「import play.db.anorm._」も必要

最初にCountryテーブルを表すCountryというcaseクラスを作成する。

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

(訳注:IdとPkの役割の違いがこの文書では明確ではないが、おそらくのところ、 Pkはプライマリキーであるが、必ずしも値は無くてもよい、つまりこの値でレコードを特定することはできないもの。 Idはプライマリキーで必ず値が存在し(IdはPkのサブクラス)、それによってレコードを特定することができるもの。。。らしい。 したがって、通常の使用ではIdを使っておけば間違いないと思われる。

既存テーブルのすべての列をcaseクラスで定義する必要はないことに注意する。サブセットで十分だ。

さて、自動的にCountryのパーサを得るために、Magicを継承するオブジェクトを記述しよう。

object Country extends Magic[Country]

もし、このconventionに反して、caseクラスとは異なるテーブル名にしたいのであれば、それを指定することもできる。

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

(訳注:ここは、

object Country extends Magic[Country](tableName = Some("Countries"))

の間違い???)

Contryパーサでは単純にCountryを使う。 (訳注:確認してないけど、"select * from Country"は"select * from Countries"の間違い?)

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

訳注:これをやってみたが、改造PostgreSQLドライバの場合(Play/AnormPostgreSQL)にはとにかく遅くて使い物にはならない。 改造Firebirdドライバ(Play/AnormFirebird)では問題ないようだ。

Magicは自動的に基本的なSQLクエリを生成するメソッドのセットを提供してくれる。

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はupdateやinsertのメソッドも提供してくれる。例えば、

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

(訳注:これは大して便利ではない。更新の意図の無いフィールドの値もすべて書き込まれてしまうので、 それらの値も用意しておかなければならない。他にやり方があるのだろうか?)

最後に、未記述のCityやCountryLanguageのcaseクラスを書き、もっと複雑なクエリを行ってみよう。、

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

コメント

※オリジナルの記事に投稿されているコメントの訳。

Przemysław Pokrywkaさんのコメント

タイプセーフなDSLの本当のメリットは集中化メタモデルにある(centralized metamodel)のであって、その正当性じゃない。 もしスキーマが変更されたらどうする?SQLの記述を探しまわって修正し、トライ&エラーを繰り返すのか? あるいは、メタモデルを変更して、コンパイラに「変更する必要があるよ」と警告してもらうのか?

他の部分ではAnormはいくつもナイスなアイデアがあるとおもうけど、僕にとってタイプセーフティが無いのは致命的だな。 リファクタリングが難しい上に、DRY原則にも反している。 それを使うすべての場所で列の型を繰り返さなければいけないからね(例えば、 firstRow[Long]("c"))。

さらに、SQLが文字列の形では、SQLステートメントの「部分」を再利用したくてもできないよ。 もしplayを使うとしても、僕はデータレイヤには、Anormではなくて、Squeryl / QueryDSL / ScalaQueryを使うだろうねえ。

(訳注というか意見: ソフト屋さん達の行動は全く不思議なものだと思う。タイプセーフでない要素がプログラム全体に広がってしまうスクリプト言語は野放しにされている一方で、一部分でもタイプセーフでなければ反対する人がいる。 こういう人はまず、rubyやphpといった言語を撲滅する運動を推進すべきではなかろうか。 それを切に願う。