Play で Scala を使う方法

閲覧

内容

バージョン選択

Anorm:Play-Scala での SQL データアクセス

Scala モジュールは、 Anorm と呼ばれる、まったく新しいデータアクセス層を持っています。これは、プレーンな SQL を使ってデータベースへのリクエストを行い、応答結果のデータセットを解析・変換するいくつかの API を提供するものです。

Anorm は ORマッパーではありません

本章では、MySQL world sample database を使います。

これをアプリケーションで使えるようにするには、MySQL の WEBサイトの紹介に従ってください。そして、 conf/application.conf ファイルに以下の設定行を追加することで使えるようになります。

db=mysql:root@world

概要

今日び、SQL データベースにピュアな古い SQL を使ってアクセスすることは、退化しているようで奇妙に感じるかもしれません。とりわけ、SQL アクセスを完全に隠蔽している、Hibernate のような高度な OR マッパーを使うことに慣れた JAVA 開発者にとっては尚更でしょう。

JAVA でこれらのツールがいつも必要とされるとしたら、Scala のような高度なプログラミング言語の恩恵を受けている時にこれらのツールは必要とされず、逆にすぐさま非生産的になってしまうでしょう。

JDBC を使うのは辛いが、すばらしい API を提供する

JDBC API を直接使うことが退屈なのは認めます。特に JAVA では。そこかしこで例外をチェックしなければなりませんし、行データセットを扱いたいデータ構造に変換するために ResultSet を繰り返しループさせなければいけません。

しかし、例外に悩まされないで済む Scala を使い、関数言語でデータ変換を本当に簡単にすることができる、JDBC のためのよりシンプルな API を提供します。実は、そこが Play-Scala の SQL アクセス層が位置するところであり、 Scala 構造に JDBC データを効率的に変換するいくつかの API を提供するのです。

RDB にアクセスするために、これ以上 DSL は要らない

SQL は既に RDB にアクセスするための最良の DSL です。我々はもはや何か新しいものを発明する必要はありません。しかも、データベースベンダーによって SQL 文法と機能が異なります。

DSLのような別のオリジナル SQL でこれを抽象化しようとしたら、( Hibernate のように )それぞれのベンダーで作られたいくつかの方言を扱わなければならなかったり、個々のデータベースの機能をあきらめなければいけなかったりします。

Play-Scala は、時として埋め込み済みの SQL 文を提供します。しかし、内部で SQL を使っているという事実を隠すためのものではありません。ささいなクエリを入力する文字列をとっておくだけのもので、いつでも旧来のプレーンな SQL に戻ることができます。

SQL を生成するためのタイプセーフな DSL は間違っている

時々、タイプセーフな DSL はコンパイラーでチェックしてくれるので、ベターだと言う人がいます。不幸なことにコンパイラーは、あなた自身でデータ構造をデータベーススキーマにマッピングさせるように書きあげるメタモデル定義に従って、クエリをチェックしているのです。

そこにはメタモデルが正しいことの保証はまったくありません。たとえコンパイラーがコードとクエリが正しく記載されていると言ったとしても、実際のデータベース定義とのミスマッチのせいで、無様にもランタイムエラーが発生し得るのです。

SQL コードをコントロールせよ

OR マッパーは通常はうまくいくでしょう。しかし、複雑なスキーマや既存のデータベースを扱う時には、OR マッパーが意図する SQL クエリを生成するようための格闘すに多くの時間を費やすことになってしまうでしょう。

単純な「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 値を返します。

update クエリは 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 は複数行 String をサポートしているので、複雑な SQL でも自由に書くことができます。

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

もうひとつのやり方として、変数をポジションで埋めることもできます。

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

Stream API を使ってデータを取り出す

Select クエリで取得したデータにアクセスする、第1の方法は、 Stream API を使うことです。

SQL 文で apply() を呼べば、 RowStream を遅延受取して、各行を辞書のように捉えることができます。

// Create an SQL query
val selectCountries = SQL("Select * from Country")
 
// Transform the resulting Stream[Row] as a List[(String,String)]
val countries = selectCountries().map(row => 
    row[String]("code") -> row[String]("name")
).toList

以下の例では、データベース内の国数をカウントしています。結果は、単一列につき、単一行となります。

// 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(...) は一部の関数が定義されていないケースを無視していいるので、期待しない行を安全に無視することができています。

Null 値を許容するカラムを扱う

データベーススキーマの中で、カラムが Null 値を含む場合には、 Option 型として扱う必要があります。

例えば以下のように、 Country テーブルの indepYear は、Null 値を許容するので、 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 Parsers 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 カラムを Stringpopulation カラムを Int として、各行に対して解析を行います。ここでは、 ~< を同じ行を読むいくつかのパーサーを結合させるために使っています。

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

このように、国名と人口のリストを返すためのクエリの実行結果の型は、@List[String~Int]@ になります。

あるいは Symbol を使って以下のように書き換えることもできます。

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 をパースする時、全てのインプットを扱わないといけません。もし、パーサーが全ての利用可能なインプットを使わない場合は、エラーがスローされます。これによって、知らないうちにパーサーが失敗することを回避します。

インプットのごく一部だけをパースしたければ、 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 を使ったクエリのように、いくつかの結果業をひとつのアイテムに生成することがパーサーには求められます。このパーサーを構築するために、 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 }
                )
        } ?
    )
    
}

これで、 world というサンプルデータベース上でこれを試すと、以下の結果を取得することができます。

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

Magic[T] を追加

これらのコンセプトをベースとして、パーサーを書くのを助けてくれる、 Magic ヘルパーを Play フレームワークは提供してくれます。これはデータベーステーブルをマッチングさせる case class を定義したら、 Play-Scala がパーサーを生成してくれるというものです。

Magic パーサーは、 Scala 構造データを、データベーススキーマにマッピンさせる規約が必要になります。 この例では、 Scala の Case クラスをテーブルにマッピングさせるデフォルトの規約を使います。規約では、クラス名をテーブル名として使い、フィール名をカラム名として使います。

次に行く前に、インポートが必要です。

import play.db.anorm.defaults._

Country テーブルを記述した最初の Country Case クラスを定義してみましょう。

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

ちなみに、全てのテーブルカラムを Case クラスで指定する必要はありません。その一部で十分です。

さて、 Magic を拡張して、 Country のパーサを自動的に取得するオブジェクトを作ってみましょう。

object Country extends Magic[Country]

もし規約を壊し、 Country Case クラスのために違うテーブル名を使いたかったら、それを指定することができます。

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

また、 Country パーサーとして、単純に Country を使うことができます。

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()
val c:List[Country] = Country.find("population > 1000000").list()
val c:Option[Country] = Country.find("code = {c}").on("c" -> "FRA").first()

また、 Magicupdate メソッドと、 insert メソッドを提供します。例えば、

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

最後に、足りない City Case クラスと、 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?")

コメント

このフォームを使用して、このページのドキュメントに関する修正や追記、提案を追加してください。 質問はここには書かず、play-framework グループに質問してください。 サポート要求、バグレポート、趣旨に沿わないコメントは警告無しに削除されます。