ラベル scala の投稿を表示しています。 すべての投稿を表示
ラベル scala の投稿を表示しています。 すべての投稿を表示

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モジュールを使用した記事なので、本当に参考までと考えたほうがよいです。

2011年7月23日土曜日

[scala]簡易的な実行速度測定

scala で書いたコードの実行速度を測定したい場合に使える、簡易的なメソッドを考えたので、そのメモ。 内容は次のような感じです。
def timeOf[A](f: => A): A = {
  val start = System.currentTimeMillis
  val result = f
  val end = System.currentTimeMillis
  println("[Time] %s ms".format(end - start))
  result
}
これを、たとえば適当な package.scala に書いて使えるようにしておけば
scala> timeOf((1 to 100000000).foreach(_ * 2))
[Time] 1067 ms
などというように使えます。
※package.scala は、簡単に説明しますと、あるパッケージで使い回したい変数やメソッドをまとめて定義しておける場所です。こちらのブログなどを参照してください:
Scala 2.8 で追加されたパッケージオブジェクト

上記のように定義すると
val result = timeOf {
  // なんだか長いコード
}
// 以下、result を使った処理
というふうにも書けるので便利かと思います。

scala が標準で用意している scala.testing.Benchmark という trait もありますが、 こちらは mixin したあとに、runメソッドをオーバーライドして、runBenchmark を呼び出す、という手順を挟むので、もう少し気軽に試したいなぁ、と思って書いてみた次第です。あと、Benchmark の場合は、処理実行結果を返してくれませんが、上記のメソッドだと処理結果も使うことができます。また、ソースをみたときに気づいた点として最後に Platform.collectGarbage(= System.gc()) を呼び出しているようです。

実際に scala.testing.Benchmark を使った場合のサンプルは以下のサイトで丁寧に説明されています。
参考:Scala Benchmark

2011年7月9日土曜日

play-scalaの導入

play-scala プロジェクトを新規に作成して,eclipse で開発できるように環境を構築していたのですが,途中,エラーが出て少し悩んだところがあったのでまとめておきます.
※簡略できる手順などありましたら,教えていただけると助かります.

環境

※バージョン管理ツールに svn を使用しますが,svn の環境についてはすでに準備されているものとします.

手順

  1. play-scala プロジェクトの新規作成

    詳細な play のインストールについては,本家サイトを参照ください.
    ここでは,scala module を使用したプロジェクトの新規作成についてのみ記述します.新規作成を行うには play new コマンドに with オプションにて,scala を指定します.
    $ play new hoge --with scala
    
    次のように表示されればOKです.
    ~        _            _ 
    ~  _ __ | | __ _ _  _| |
    ~ | '_ \| |/ _' | || |_|
    ~ |  __/|_|\____|\__ (_)
    ~ |_|            |__/   
    ~
    ~ play! 1.2.2, http://www.playframework.org
    ~
    ~ The new application will be created in /some/workspace/hoge
    ~ What is the application name? [hoge] 
    ~
    ~ Resolving dependencies using /some/workspace/hoge/conf/dependencies.yml,
    ~
    ~       play->scala 0.9.1 (from playLocalModules)
    ~
    ~ Installing resolved dependencies,
    ~
    ~       modules/scala-0.9.1 -> /opt/play-1.2.2/modules/scala-0.9.1
    ~
    ~ Done!
    ~
    ~ OK, the application is created.
    ~ Start it with : play run hoge
    ~ Have fun!
    ~
    
    プロジェクトが作成できたので,svn リポジトリに登録しておきます.
    $ svn import ./hoge http://somewhere/svn/hoge/trunk/hoge -m "new project"
    
  2. play-scala プロジェクトを eclipse プロジェクトに変換する.

    play-scala プロジェクトを eclipse で扱えるようにするためには,コマンドで eclipse プロジェクト化する必要があります.
    まず eclipse に取り込む前に,一度チェックアウトしておきます.
    $ svn co http://somewhere/svn/hoge/trunk/hoge /some/hoge
    $ cd /some/hoge
    $ play eclipsify
    
    これで..classpath, .projectなどのファイルや eclipse からの起動に必要な launcher が納められた eclipse ディレクトリが作成されたものと思います.
  3. eclipse へ import

    あとは,取り込むだけといきたいのですが,ここでそのままパッケージ・エクスプローラにて,[右クリック]→[インポート]→[既存プロジェクトをワークスペースへ]から eclipse に import すると,次のようなエラーが発生します.
    value Application is not a member of package views
    
    これを回避するためには,プロジェクトを起動させ,scala ファイル(および class ファイル)を自動生成させる必要があります.
    エラーは無視して,eclipse 上からプロジェクトを起動させます.起動させるためには,hoge/eclipse/hoge.launch を右クリックから実行させればOKです.コンソールに
    Listening for transport dt_socket at address: 8000
    08:24:00,422 INFO  ~ Starting /some/workspace/hoge
    08:24:00,446 INFO  ~ Module scala is available (/opt/play-1.2.2/modules/scala-0.9.1)
    08:24:01,431 INFO  ~ Scala support is active
    08:24:01,432 WARN  ~ You're running Play! in DEV mode
    08:24:01,471 INFO  ~ Listening for HTTP on port 9000 (Waiting a first request to start) ...
    
    のように表示されたら,ブラウザから http://localhost:9000 にアクセスします.そこで再度コンソールを見ると
    Compiling:
     /some/workspace/hoge/app/controllers.scala
     /opt/play-1.2.2/modules/docviewer/app/helpers/CheatSheetHelper.java
     /opt/play-1.2.2/modules/docviewer/app/DocViewerPlugin.java
     /some/workspace/hoge/tmp/generated/views.defaults.html.welcome.scala
     /some/workspace/hoge/tmp/generated/views.html.main.scala
     /opt/play-1.2.2/modules/docviewer/app/controllers/PlayDocumentation.java
     /some/workspace/hoge/tmp/generated/views.Application.html.index.scala
    Traversing /some/workspace/hoge/app/controllers.scala
    Traversing /some/workspace/hoge/tmp/generated/views.Application.html.index.scala
    Traversing /some/workspace/hoge/tmp/generated/views.defaults.html.welcome.scala
    Traversing /some/workspace/hoge/tmp/generated/views.html.main.scala
    API phase took : 0.114 s
    08:24:21,920 INFO  ~ Application 'hoge' is now started !
    
    のように自動でコンパイルが行われていることがわかります.あとは,hoge プロジェクトを右クリックして,[プロパティー]→[Java のビルド・パス]→[ライブラリー]→[クラス・フォルダーの追加]から,hoge/tmp/classes を選択して,[OK]を選べばエラーが発生しなくなります.
  4. eclipse 設定ファイルを管理対象外にする

    最後に,play eclipsify にて生成されたファイルやプロジェクト起動時に自動生成されたファイルを svn へコミットしないようにしておきます.管理対象外とするのは,以下のファイルおよびディレクトリです.
    • hoge/.settings/
    • hoge/eclipse/
    • hoge/tmp/
    • hoge/.classpath
    • hoge/.project
    • hoge/.scala_dependencies
    上記のファイルおよびディレクトリを選択し,右クリック→[チーム]→[svn:ignore に追加]とすればOKです.