Scalatra + Scalate + Morphia で MongoDB を使った Web アプリ開発
以前、Sinatra(id:fits:20110306)や express(id:fits:20110409)で作成したサンプルと同じものを Scalatra + Scalate(Scaml 使用)+ Morphia の構成で作成してみました。
環境は以下の通りです。
- Scala 2.9.0.1
- sbt 0.7.7
- Scalatra 2.0 Snapshot
- Scalate 1.6 Snapshot
- Morphia 1.0 Snapshot
- MongoDB 1.9.0
サンプルのソースは 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" }
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>