Scalatra + Scalate + Morphia で MongoDB を使った Web アプリ開発

以前、Sinatraid:fits:20110306)や express(id:fits:20110409)で作成したサンプルと同じものを Scalatra + Scalate(Scaml 使用)+ Morphia の構成で作成してみました。

環境は以下の通りです。

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20110521/
なお、ソースには Servlet 3.0 版(scalatra-scalate-morphia_servlet3)も用意していますが、こちらは sbt の jetty-run では正常に動作しないので sbt package で war 化して Tomcat 等で実行する必要があります。

設定ファイルの作成

まず、sbt を使ってプロジェクトを作成し、以下のように Scalatra, Scalate, Morphia を使うための設定を行います。(Scala2.9.0.1を使用するには、sbt でのプロジェクト作成時に 2.9.0-1 というバージョンを入力する点に注意

Web アプリ作成用の DefaultWebProject を extends し、jetty で動作確認するので jetty の設定も行います。

なお、sbt package で war ファイルを作る場合、デフォルトでは scala-compiler.jar が含まれずに Scalate が正常に動作しないため、webappClasspath をオーバーライドして war ファイルに scala-compiler.jar を追加するようにしている点に注意。

project/build/ScalatraMorphiaSampleProject.scala
import sbt._

class ScalatraMorphiaSampleProject(info: ProjectInfo) extends DefaultWebProject(info) {
    //以下の設定で war ファイルに scala-compiler.jar が入るようになる
    override def webappClasspath = super.webappClasspath +++ buildCompilerJar

    val jettyVersion = "8.0.0.M3"
    val scalatraVersion = "2.0.0-SNAPSHOT"
    val scalateVersion = "1.6.0-SNAPSHOT"
    val morphiaVersion = "1.00-SNAPSHOT"

    //jetty 8 を使うための設定
    //(groupId を org.eclipse.jetty とする点に注意)
    val jettyWebapp = "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "test"
    val servletapi = "javax.servlet" % "servlet-api" % "2.5" % "provided"

    //Scalatra
    val scalatra = "org.scalatra" %% "scalatra" % scalatraVersion
    //Scalatra の Scalate サポートモジュール
    val scalatraScalate = "org.scalatra" %% "scalatra-scalate" % scalatraVersion

    //Scalate
    val scalate = "org.fusesource.scalate" % "scalate-core" % scalateVersion

    //Morphia
    val morphia = "com.google.code.morphia" % "morphia" % morphiaVersion

    //Scalatra のリポジトリ設定
    val sonatypeNexusSnapshots = "Sonatype Nexus Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
    val sonatypeNexusReleases = "Sonatype Nexus Releases" at "https://oss.sonatype.org/content/repositories/releases"

    //Scalate のリポジトリ設定
    val fuseSourceSnapshots = "FuseSource Snapshot Repository" at "http://repo.fusesource.com/nexus/content/repositories/snapshots"

    //Morphia のリポジトリ設定
    val morphiaSnapshot = "Morphia Repo at Google Code" at "http://morphia.googlecode.com/svn/mavenrepo"
}

次に web.xml ファイルを作成し、以降で作成する Servlet の設定を行います。

src/main/webapp/WEB-INF/web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> 

  <servlet>
    <servlet-name>sample</servlet-name>
    <servlet-class>fits.sample.ScalatraMorphiaSample</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>sample</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

モデルクラスの作成

モデルクラスは id:fits:20110515 で作成したものをパッケージ名だけを変更してそのまま使う事にします。

src/main/scala/models/Book.scala
package fits.sample.models

import java.util.{List, ArrayList}
import com.google.code.morphia.annotations._
import org.bson.types.ObjectId

@Entity(value = "books", noClassnameStored = true)
class Book(var title: String, var isbn: String) {

    def this() = this("", "")

    @Id var id: ObjectId = null

    @Embedded var comments: List[Comment] = new ArrayList[Comment]()
}

Scalatra による Web サーバー処理

Scalatra を使った処理は以下のようになります。

Scalate を使うために ScalateSupport を with し、レイアウトファイルを適用するため templateEngine.layout() を使っています。(renderTemplate() だとレイアウトファイルが適用されない模様)

src/main/scala/ScalatraMorphiaSample.scala
package fits.sample

import scala.collection.JavaConverters._
import org.scalatra.ScalatraServlet
import org.scalatra.scalate.ScalateSupport
import com.google.code.morphia.Morphia
import com.mongodb.Mongo
import org.bson.types.ObjectId

import fits.sample.models._

class ScalatraMorphiaSample extends ScalatraServlet with ScalateSupport {
    //MongoDBへの接続設定
    val db = new Morphia().createDatastore(new Mongo("localhost"), "book_review")

    beforeAll {
        contentType = "text/html"
    }

    //Topページ表示
    get("/") {
        //全Book取得
        val books: Iterable[Book] = db.find(classOf[Book]).order("title").asList.asScala
        //全User取得
        val users: Iterable[User] = db.find(classOf[User]).order("name").asList.asScala
        //レイアウトファイルを使ったページ描画
        templateEngine.layout("index.scaml", Map(
            "books" -> books,
            "users" -> users
        ))
    }

    //Bookページ表示
    get("/books") {
        val books: Iterable[Book] = db.find(classOf[Book]).order("title").asList.asScala

        templateEngine.layout("book.scaml", Map(
            "books" -> books
        ))
    }

    //Book追加
    post("/books") {
        db.save[Book](new Book(params("title"), params("isbn")))

        redirect("books")
    }

    //Comment追加
    post("/comments") {
        val book = db.get(classOf[Book], new ObjectId(params("book")))
        val user = db.get(classOf[User], new ObjectId(params("user")))

        book.comments.add(new Comment(params("content"), user))
        db.save[Book](book)

        redirect(".")
    }
    ・・・
}

Scalate(Scaml) によるページの定義

Scaml での HTML エスケープ処理の動作は Haml.js と同じようです。

  • = で HTML エスケープされる
  • != で HTML エスケープされない

繰り返し処理は Scala の for が使えます。

なお、使用する変数の宣言(下記サンプルの "-@ val body: String" 箇所)が必要な点に注意。

レイアウトファイルは WEB-INF/scalate/layouts/ に配置します。Haml.js と同様に body 変数に各々のページ内容が設定されます。

src/main/webapp/WEB-INF/scalate/layouts/default.scaml (レイアウト定義)
-@ val body: String
!!! 5
%html
    %title Scalatra + Scalate + Morphia Sample
    %body
        != body

body の内容は HTML エスケープさせずにそのまま出力させたいので != を使っています。


Top ページは以下の通りです。
templateEngine.layout() の第二引数で設定されたバインド変数を使うために、変数宣言が必要となる点に注意が必要です。
なお、Haml と同様に #{・・・} の記述が使えます。

src/main/webapp/WEB-INF/index.scaml (Topページ定義)
- import fits.sample.models._
-@ val books: Iterable[Book]
-@ val users: Iterable[User]

.menu Menu
%ul
  %li
    %a(href="books") Books List
  %li
    %a(href="users") Users List

.list Book Comments
%form.post(action='comments' method='post')
  %select(name='user')
    - for(u <- users)
      %option(value='#{u.id}')= u.name
  %select(name='book')
    - for(b <- books)
      %option(value='#{b.id}')= b.title
  %input(name='content' type='text')
  %input(type='submit' value='Add')

- for(b <- books)
  %ul
    %li= b.title
    %ul
      - for(c <- b.comments)
        %li
          #{c.content} : #{c.user.name}, #{c.createdDate}

実行

まず、MongoDB を実行しておきます。

MongoDB 実行例
> mongod -dbpath db

次に、sbt の jetty-run コマンドでビルドと実行を行います。

jetty-run による実行例
・・・> sbt
[info] Building project scalatra-scalate-morphia-sample 1.0 against Scala 2.9.0-1
[info]    using ScalatraMorphiaSampleProject with sbt 0.7.7 and Scala 2.7.7
> jetty-run

停止させるときは jetty-stop を実行します。(jetty-restart や jetty-reload もあります)

http://localhost:8080/ に接続すると Sinatra 版(id:fits:20110306)や express 版(id:fits:20110409)のサンプルと同様の画面が表示されます。

Top ページの表示 HTML 例
<!DOCTYPE html>
<html>
  <title>Scalatra + Scalate + Morphia Sample</title>
  <body>
    <div class="menu">Menu</div>
    <ul>
      <li>
        <a href="books">Books List</a>
      </li>
      <li>
        <a href="users">Users List</a>
      </li>
    </ul>
    <div class="list">Book Comments</div>
    <form class="post" action="comments" method="post">
      <select name="user">
        <option value="4dd13c6a3d297b41fda3ca3f">tester1</option>
        <option value="4dd13c723d297b41fea3ca3f">テスター</option>
      </select>
      <select name="book">
        <option value="4dd149893d297b41def1f261">ドメイン駆動設計</option>
        <option value="4dd149663d297b41ddf1f261">プログラミングScala</option>
      </select>
      <input name="content" type="text"/>
      <input type="submit" value="Add"/>
    </form>
    <ul>
      <li>ドメイン駆動設計</li>
      <ul>
        <li>
          1 : tester1, 2011年5月17日
        </li>
        <li>
          test : テスター, 2011年5月17日
        </li>
      </ul>
    </ul>
    <ul>
      <li>プログラミングScala</li>
      <ul>
        <li>
          aaa : tester1, 2011年5月17日
        </li>
        <li>
          テストコメント : テスター, 2011年5月17日
        </li>
      </ul>
    </ul>
  </body>
</html>