2012年1月8日日曜日

[scala][play2.0] Anorm のサンプル(1)

playframework に付いてくるDBアクセスモジュール(ORマッパーではない) Anorm を使ってみました。
よくわかっていなところが多いですが、メモ書きしておきます。

テストプロジェクトの構築


今回使用した環境は、play2.0 になります。インストール手順については省きますので、他のサイトをご覧になって、各自準備してください。
リポジトリは、こちらです。HEADを使用しています。
https://github.com/playframework/Play20

はじめに、動作確認用のテストプロジェクトを作成します。
$ play new anorm-sample

テーブルとモデルの準備


使用したテーブルは次の通り。
CREATE TABLE user (
    id bigint NOT NULL AUTO_INCREMENT, 
    email varchar(255) NOT NULL, 
    password varchar(255) NOT NULL, 
    fullname varchar(255) NOT NULL, 
    isAdmin boolean NOT NULL, 
    PRIMARY KEY (id)
);

ファイル名は 1.sql として、arnom-sample/db/evolutions/default/1.sql に配置します。
テスト時にはこのテーブルを使うことになります。
(完全な内容は、github においておきます)

CREATE TABLE user (
    id bigint NOT NULL AUTO_INCREMENT, 
    email varchar(255) NOT NULL, 
    password varchar(255) NOT NULL, 
    fullname varchar(255) NOT NULL, 
    isAdmin boolean NOT NULL, 
    PRIMARY KEY (id)
);


テーブルに対応するモデルを実装します。

まずは、caseクラスです。Models.scala ファイルに定義しました。
import anorm._

case class User(
  id: Pk[Long] = NotAssigned, 
  email: String, password: String, fullname: String, isAdmin: Boolean)


ここで、Pk[Long] とありますが、これは、anorm パッケージに定義されている抽象クラスで、Option のようなものみたいです。
id が存在するかしないかを表現しています。実際に値が存在する場合(DBから取得したデータなど)は、
case class Id[ID](id: ID) extends Pk[ID] 
となり、
値が存在しない場合(DBに登録前のデータなど)は、
case object NotAssigned extends Pk[Nothing] 
として扱います(IDは型パラメータ)。
上記では、デフォルト値として、NotAssigned を割り当てています。

次に、Userオブジェクトを定義します。
object User {
  // 変数名は何でも構いません。
  val simple = {
    get[Pk[Long]]("user.id") ~
    get[String]("user.email") ~
    get[String]("user.password") ~
    get[String]("user.fullname") ~
    get[Boolean]("user.isAdmin") map {
      case id ~ email ~ password ~ fullname ~ isAdmin => User(id, email, password, fullname, isAdmin)
    }
  }
}


ここで、simple という変数を定義しましたが、これを用いて、SQL実行結果をパースすることになります(取得された値をcaseクラスに割り当てる)。
メソッドが何もないので、登録と検索を行う処理を定義します。
import play.api.db._
import play.api.Play.current

object User {
  
  def insert(user: User) = {
    DB.withConnection { implicit connection =>
      SQL(
        """
          insert into user(email, password, fullname, isAdmin) 
          values ({email}, {password}, {fullname}, {isAdmin})
        """
      ).on(
        'email -> user.email, 
        'password -> user.password, 
        'fullname -> user.fullname, 
        'isAdmin -> user.isAdmin
      ).executeUpdate()
    }
  }

  def findById(id: Long): Option[User] = {
    DB.withConnection { implicit connection =>
      SQL("select * from user where id = {id}").on('id -> id).as(User.simple.singleOpt)
    }
  }

  def findAll(): Seq[User] = {
    DB.withConnection { implicit connection =>
      SQL("select * from user").as(User.simple *) 
    }
  }
  
}

ソースを見れば何となく分かりそうな感じもすると思いますが、個別に補足していきます。

各メソッドは DB.withConnection で始まっていますが、これがお決まりの書き方で、java.sql.Connection を必要とするような処理ブロックを受け取ります。
このブロックの中で、発行するSQL文を組み立てて、実行することになります。
また、一連の処理をトランザクション切って実行したい場合は、DB.withTransaction を利用することになります(今回は使用しませんでしたが、そのうち書こうと思います)。

メソッド内にて発行したいsql文は anorm.SQL を使用して組み立てることになり、プレースホルダー部分は '{' と '}' で囲みます。
プレースホルダーに割り当てたい値は、on メソッドを呼び出した中でマッピングを列挙していくことになります。
基本的には、プレースホルダーの文字列に相当する scala.Symbol と、メソッドの引数で受け取った値などを組みとしてタプルを構成すればよいです。
ちなみに、anorm のソースを見てみると、scala.Symbol でなくてもよいようで、別のオブジェクトの場合は、toString の結果を使用してタプルを構成するみたいですね。
とはいっても、通常は常套手段に則り、シンボルを使用するのがいいのではないでしょうか(シンボルではない値を使用する場合があるとすれば、条件により更新対象のカラム名を変えるときとか?)

組み立てたクエリを実行するには、登録・更新系の場合であれば executeUpdateメソッドを、参照系の場合であれば asメソッドを呼び出すことになります。
それぞれ最終的には、java.sql.PreparedStatement の executeUpdateメソッドや executeQueryメソッドを呼び出しているようです。

登録・更新系の処理であれば実行結果の行数をそのまま返せばよいと思いますが、参照系の場合は最初に定義した simple を利用して、java.sql.ResultSet の内容を適切なオブジェクトに変換して返す必要があります。上記のサンプルでいうと、id を指定してデータを取得する findById においては、User.simple.singleOpt メソッドを呼び出して、Option でラップして1件返すようにしています。また、登録済みのデータすべてを取得する findAll においては、User.simple.* メソッドを呼び出して、Seq[User] を返すようにしています(実際には、List)。

ちょっと長くなってきましたので、テストケースの説明などは省略します。今回作成したサンプルコードは以下においておきます。
https://github.com/dnoguchi/anorm-sample

参考URL:
playframework 2.0 のリポジトリ
https://github.com/playframework/Play20
Play で Scala を使う方法 ー データモデルの最初のイテレーション.
http://playscalaja.appspot.com/documentation/0.9.1/guide2
※play1.x系のscalaモジュールを使用した記事なので、本当に参考までと考えたほうがよいです。

0 件のコメント:

コメントを投稿