Play framework の Tutorial を Scala と Scalate で行う(Step1, Step2)

Scala を使い始めて Java 文化*1に辟易している方は絶対に使った方が良い Play framework の Tutorial を行ってみたので、そのメモを残す。Play framework は、1.1 の beta version がインストール済みであると仮定する。

Tutorial を始める前の下準備

Play framework で ScalaScalate を使うためにモジュールをインストールする。

% sudo play install scala
% sudo play install scalate

OSX 環境で下記のエラーが出る場合は、環境設定から Proxy を外すなどで対処。10.5 から 10.6 へ更新した MBP では出ないが、10.6 が始めから入っていた Macmini では出た。

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/urllib.py", line 1425, in proxy_bypass_macosx_sysconf
      mask = int(m.group(2)[1:])

ちなみに play コマンドは Java ではなく Python で記述されているので、とても軽快。重量級の sbt から解放されるので幸せ。

Starting up the project (Scala version)

http://www.playframework.org/documentation/1.1-trunk/scguide1

Tutorial の通りに行うと scalate が使われないので下記のようにする。

% play new yabe --with scala,scalate

その後、app/controllers.scala の Application オブジェクトが継承している Controller を ScalateController に変更する。これを怠ると Controller の render メソッドが実行され index.html が存在しないと怒られる。今回は scalate を使いたいので index.ssp を読み込んで欲しい。

//..snip..
object Application extends ScalateController {
//..snip..

後は、Tutorial の通りに進めるだけ。

A first iteration for the data model (Scala version)

http://www.playframework.org/documentation/1.1-trunk/scguide2

User モデルとテストの作成で幾つかコンパイルエラーになる箇所があるため、その修正を行う必要がある。

createAndRetrieveUser

app/models/User.scala の下記は削除。

import javax.persistence._

また、後に Users オブジェクトの find メソッドを使うので app/models/User.scala には下記を追加しておく。

object Users extends QueryOn[User]

test/BasicTest.scala の下記の行を…

val bob = find[User]("byEmail", "bob@gmail.com").first

下記のように修正する。

var bob = Users.find("byEmail", "bob@gmail.com").first

Users.find の戻り値は Option クラス(ようは Some か None)であるため、下記を…

assertEquals("Bob", bob.fullname)

下記に変更する必要がある。

assertEquals("Bob", bob.get.fullname)
tryConnectAsUser

app/models/User.scala で下記をインポートし User オブジェクトを作成するよう指示があるが、これは無視。

import play.db.jpa.QueryFunctions._

前述で追加した Users オブジェクトに connect メソッドを追加する。

object Users extends QueryOn[User] {
  def connect(email: String, password: String) = {
    find("byEmailAndPassword", email, password).first
  }
}

test/BasicTest.scala の tryConnectAsUser は下記のようにする。assertNotEquals とか欲しいなぁ。

  @Test
  def tryConnectAsUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save()

    // Test 
    assertEquals("Bob", Users.connect("bob@gmail.com", "secret").get.fullname)
    assertEquals(None, Users.connect("bob@gmail.com", "badpassword"))
    assertEquals(None, Users.connect("tom@gmail.com", "secret"))
  }
ScalaTest

ここまで書いて、Play Scala Module の Yabe サンプルが ScalaTest である事に気がつく。test/BasicTest.scala を下記のようにする。

import play.test._
import models._
import org.scalatest.matchers.ShouldMatchers
 
class BasicTest extends UnitFlatSpec with ShouldMatchers {
  it should "create and retrieve a user" in {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save()

    // Retrieve the user with bob username
    var bob = Users.find("byEmail", "bob@gmail.com").first

    // Test 
    bob should not be (None)
    "Bob" should equal (bob.get.fullname)
  }

  it should "call connect on User" in {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save()

    // Test 
    Users.connect("bob@gmail.com", "secret") should not be (None)
    Users.connect("bob@gmail.com", "badpassword") should be (None)
    Users.connect("tom@gmail.com", "secret") should be (None)
  }
}

こっちの方が、None か否かハッキリしてて良い。

createPost

app/models/Post.scala は、app/models/User.scala 同様に import を削除し、下記のような Posts オブジェクトを追加しておく。他は Tutorial のままで良い。

object Posts extends QueryOn[Post]

追加で個人的な趣味で「import java.util._」ではなく「import java.util.Date」とした。

ScalaTest に切り替えたので test/BasicTest.scala の Fixtures.deleteAll() の箇所は下記のようにする。

//..snip..
import org.scalatest.BeforeAndAfterEach
//..snip..
class BasicTest extends UnitFlatSpec with ShouldMatchers with BeforeAndAfterEach {

  override def beforeEach() {
    Fixtures.deleteAll()
  }

beforeEach は、org.scalatest.BeforeAndAfterEach のメソッドであるため忘れずに継承しておく。
テストケースは下記の通り。

  it should "create Post" in {
    // Create a new user and save it
    var bob = new User("bob@gmail.com", "secret", "Bob").save()

    // Create a new post
    new Post(bob, "My first post", "Hello world").save()

    // Test that the post has been created
    1 should equal (Posts.count())

    // Retrieve all post created by bob
    var bobPosts = Posts.find("byAuthor", bob).fetch

    // Tests
    1 should equal (bobPosts.size)

    var firstPost = bobPosts(0)
    firstPost should not be (null)
    bob should equal (firstPost.author)
    "My first post" should equal (firstPost.title)
    "Hello world" should equal (firstPost.content)
    firstPost.postedAt should not be (null)
  }
postComments

app/models/Comment.scala は他のモデルと注意点が同じであるため説明省略。Comments オブジェクトの追加だけは忘れずに。
テストケースは下記の通り。

  it should "post Comments" in {
    // Create a new user and save it
    var bob = new User("bob@gmail.com", "secret", "Bob").save()

    // Create a new post
    var bobPost = new Post(bob, "My first post", "Hello world").save()

    // Post a first comment
    new Comment(bobPost, "Jeff", "Nice post").save()
    new Comment(bobPost, "Tom", "I knew that !").save()

    // Retrieve all comments
    var bobPostComments = Comments.find("byPost", bobPost).fetch

    // Tests
    2 should equal (bobPostComments.size)

    var firstComment = bobPostComments(0)
    firstComment should not be (null)
    "Jeff" should equal ( firstComment.author)
    "Nice post" should equal ( firstComment.content)
    firstComment.postedAt should not be (null)

    var secondComment = bobPostComments(1)
    secondComment should not be (null)
    "Tom" should equal ( secondComment.author)
    "I knew that !" should equal (secondComment.content)
    secondComment.postedAt should not be (null)
  }

ここで、tmp の下に Comment.class が作成されずにエラーが出てしまったので「play test」を再実行している。

useTheCommentsRelation

app/models/Post.scala を下記のように修正する。

package models
 
import java.util.{Date, List=>JList, ArrayList}
import play.db.jpa._
 
@Entity
class Post(
    @ManyToOne
    var author: User,
    var title: String,
    @Lob
    var content: String
) extends Model {
  var postedAt: Date = new Date()
  def this() = this(null, null, null)

  @OneToMany(mappedBy="post", cascade=Array(CascadeType.ALL))
  var comments: JList[Comment] = new ArrayList[Comment]

  def addComment(author: String, content: String) = {
    val newComment = new Comment(this, author, content)
    newComment.save()  
    comments.add(newComment)
    this
  }
}

object Posts extends QueryOn[Post]

テストケースは下記の通り。

//..snip..
import scala.collection.JavaConversions._
//..snip..
  it should "use the comments relation" in {
    // Create a new user and save it
    var bob = new User("bob@gmail.com", "secret", "Bob").save()

    // Create a new post
    var bobPost = new Post(bob, "My first post", "Hello world").save()

    // Post a first comment
    bobPost.addComment("Jeff", "Nice post")
    bobPost.addComment("Tom", "I knew that !")

    // Count things
    1 should equal (Users.count())
    1 should equal (Posts.count())
    2 should equal (Comments.count())

    // Retrieve the bob post
    val getBobPost = Posts.find("byAuthor", bob).first
    getBobPost should not be (None)

    // Navigate to comments
    2  should equal (getBobPost.get.comments.size)
    "Jeff" should equal (getBobPost.get.comments(0).author)

    // Delete the post
    getBobPost.get.delete()

    // Chech the all comments have been deleted
    1 should equal (Users.count())
    0 should equal (Posts.count())
    0 should equal (Comments.count())
  }

scala.collection.JavaConversions を import しておかないと「getBobPost.get.comments(0)」の箇所で java.util.List が、そのまま使用されてしまうので気をつける。
ここは、Model 側でどうにかしたいので、後で考える。

fullTest

テストケースは下記の通り。

  it should "work if things combined together" in {
    Fixtures.load("data.yml")

    // Count things
    2 should equal (Users.count())
    3 should equal (Posts.count())
    3 should equal (Comments.count())

    // Try to connect as users
    Users.connect("bob@gmail.com", "secret") should not be (None)
    Users.connect("jeff@gmail.com", "secret") should not be (None)
    Users.connect("jeff@gmail.com", "badpassword") should be (None)
    Users.connect("tom@gmail.com", "secret") should be (None)

    // Find all bob posts
    var bobPosts = Posts.find("author.email", "bob@gmail.com").fetch
    2 should equal (bobPosts.size)

    // Find all comments related to bob posts
    var bobComments = Comments.find("post.author.email", "bob@gmail.com").fetch
    3 should equal (bobComments.size)

    // Find the most recent post
    var frontPost = Posts.find("order by postedAt desc").first

    frontPost should not be (None)
    "About the model layer" should equal (frontPost.get.title)

    // Check that this post has two comments
    2 should equal (frontPost.get.comments.size)

    // Post a new comment
    frontPost.get.addComment("Jim", "Hello guys")
    3 should equal (frontPost.get.comments.size)
    4 should equal (Comments.count())
  }

*1:JavaVM, IDE, Build Tools, Servlet Container …どれもこれも重量級