Play framework 2.0でMongoDBを使ってみる
普段はMySQLか、もしくはPostgreSQLな仕事ばかりでMongoDBを使うようなことは無かったので、一度MongoDBを触ってみようと思い、記事の作成とそこにコメントを付けていくようなアプリを書いてみた。ログインはめんどいので割愛。
play-mongo-sample
ODM
MongoDBへのアクセスには、Salatというscalaのケースクラスとの変換をやってくれるORMならぬODMを使用。Play framework(scala)からはプラグインのplay-salatを使うと簡単に扱えるようだ。1.0.9だとライブラリ依存関係の解決でエラーが出てしまう(https://github.com/leon/play-salat/issues/28)ので、1.1-SNAPSHOTで。
記事の構造
MongoDBだと埋め込みオブジェクトが持てるので、記事にコメントのオブジェクトを埋め込むような形で扱うことにする。こんな感じ。
{ title: "タイトル", body: "本文", comments: [ { body: "コメント1" }, { body: "コメント2" } ] }
Model
modelsパッケージにPost.scalaを作成し、記事の構造を定義したケースクラスと記事の操作を書いていく。
ケースクラスはこんな感じ。
case class Comment( body: String ) case class Post( @Key("_id")id: ObjectId = new ObjectId, title: String, body: String, comments: List[Comment] = List() )
SalatのModelCompanion traitを使うと基本的な操作が提供されるようで便利。さらにそこへコメントの追加処理などを定義していく。$pushを使ったupdateのMongoDBObjectの入れ子は面倒。。。
object Post extends ModelCompanion[Post, ObjectId]{ val dao = new SalatDAO[Post, ObjectId](collection = mongoCollection("posts")) {} def findOneById(id: String): Option[Post] = { try { dao.findOneById(new ObjectId(id)) }catch { case e: IllegalArgumentException => None case _ => None } } def addComment(p: Post, comment: Comment) = { update( MongoDBObject("_id" -> p.id), MongoDBObject( "$push" -> MongoDBObject( "comments" -> MongoDBObject( "body" -> comment.body ) ) ), false, false, new WriteConcern) } }
controller, form
controllers/Posts.scalaに必要なアクションを定義し、routesに追加。本当は記事内容の修正や削除なんかも必要だけど、記事の作成と埋め込みオブジェクトのコメントの追加のところをやってみたかっただけなので割愛。あと、ところどころかなりいい加減。エラー処理とか。
最初Formをこんな風に定義していて、ちょっとはまってしまった。
val postForm = Form( mapping( "id" -> ignored(new ObjectId), "title" -> nonEmptyText, "body" -> nonEmptyText )(Post.apply)(Post.unapply) )
これだとObjectIdが重複して2件目以降の保存が失敗してしまったので、次のように修正。
val postForm = Form( mapping( "title" -> nonEmptyText, "body" -> nonEmptyText ) ((title: String, body: String) => Post(title = title, body = body)) ((post: Post) => Some(post.title, post.body)) )
とりあえずPlay frameworkで触ってみたけど、アプリからMongoDBを使うところに関してはそれほど難しくなさそうな感じ。オブジェクトとのマッピングに関してはSQLよりもよっぽど自然にできそうだし。他の言語でも有名どころでは大体ODMが揃ってるみたいだし。
データモデリングに関しては、1対多の関係を配列で埋め込むようにしたり、多対多の関係をそれぞれIDの配列を持ち合うような形を基本として考えて、そこから検索の都合やデータ量の都合などが要件に合うように崩していくような感じでいいんだろうか。
参考
MongoDBの薄い本(The Little MongoDB Book)
短いチュートリアルの中に色々重要なことがまとまってるように感じました。
MongoDBにおける関連(Relation)のスキーマ設計
MongoDBでのデータモデリングについて。ここでいうパターン2やパターン4を多用できたほうがMongoDBでは扱いやすそう。
Symfony2.1.0 & Zend Framework2.0.0 リリースされました
Zend Framework 2.0.0 STABLE Released!
ということで、PHPの大物フレームワーク2つの新バージョンがリリースされたようです。
どちらもComposer対応ということで、これからComposerの利用が進んでいくといいですね。
個人的には最近はPHPではSymfony2系推しですけど、まだ全然見てないけれどもZend Framework 2.0.0のほうもZend\Diの導入や、Zend\ServiceManagerやZend\EventManager等で構築されたMVCなど、内部構造がガラっと変わっていそうで見てみないといけませんね。
フルスタックのWebアプリケーションフレームワークとしてのZend Frameworkにはプロジェクトの生成とか色々できるような使いやすいコマンドラインツールが付いていて欲しいんですがそれはどうも無さそうですかね?Zend Frameworkは1系は結構使ってたけど、開発を始める最初の構成を作るのが結構めんどくさかったんだよねぇ…。
※npmにあったのでそのうち見てみる。まだあんまり機能がなさそう?
zf2-cli A commandline tool for managing Zend Framework 2 projects
iPhoneアプリ開発の勉強始めてみた
MacBook Air 2012 13inchを購入したので、Xcode入れてiPhoneアプリ開発をいじり始めてみてる。
しかし第一歩からいきなり挫折しそうになった。iOSシミュレータインストールするためにApple Developer登録しようとして、普段使ってるApple IDで登録を進めようとしたら登録内容の名前や住所が化けてるじゃないか…。
登録内容を英語に直そうかと思ってもそれもどうもダメっぽいし、そこで諦めてその夜は寝てしまった。
翌日、開発用に別のApple IDを登録することにしてようやく先に進んだ。別のApple IDを取るときは、gmailなら+使ったエイリアスで登録できたので新しいメアドを用意したりせずに済んだ。
とりあえず今のところまだ、昨日までの帰省中にiOS Developer Libraryの日本語ドキュメントを少し見てチュートリアル通りにサンプルアプリを書いてみた程度。
で、まずはそんなに難しくなさそうな書籍でなんとなく感覚をつかんでみようと思い、書店でiPhoneアプリの入門本をいくつか見てみて、iPhoneアプリ開発塾という本を買ってみた。まずはこれを読み進めてみよう。
Play Framework 2.0.2 + AngularJS + Squeryl + lift-jsonでTODOアプリのサンプルを書いてみた
Play Framework 2.0 + AngularJS + Squeryl + lift-json を使って、AngularJSのTODOアプリのサンプルのバックエンドにPlay Framework2.0を使うような感じで、TODOアプリのサンプルを書いてみた。
tarhashi/Play-AngularJS-Sample
細かいことはソース見てください。コメント全然入ってないけど…。
Squerylのセットアップ
Squerylを使えるようにするために、以下のようなapp/GlobalSettings.scalaを作成する。今はドライバの初期化をH2 DBの場合しか書いてないが、実戦で使う場合にはPostgresなりMySQLなりを使えるようにする。
import org.squeryl.adapters.H2Adapter import org.squeryl.internals.DatabaseAdapter import org.squeryl.{Session, SessionFactory} import play.api.db.DB import play.api.Application import play.api.GlobalSettings object Global extends GlobalSettings { override def onStart(app: Application) { SessionFactory.concreteFactory = app.configuration.getString("db.default.driver") match { case Some("org.h2.Driver") => Some*1 case _ => sys.error("Database driver must be org.h2.Driver") } } def getSession(adapter: DatabaseAdapter, app: Application) = Session.create(DB.getConnection()(app), adapter) }
lift-json
どうもPlay標準のJSONモジュールは面倒な感じなので、@tototoshiさんのPlay20からlift-jsonを使えるモジュールを使ってみた。まあ、今回は大したことやってないので何使ってもよさそうでしたが。
javascriptRouter
Play Framework2.0にはjavascriptRouterという、ajax通信するときにjavascriptからroutesの定義をタイプセーフに使うための仕組みがあるので、これを使ってみる。コントローラにjavascriptRouterを出力するためのメソッドを作成。
def javascriptRoutes() = Action { implicit request => import play.api.Routes Ok( Routes.javascriptRouter("jsRouter", Some("jQuery.ajax"))( routes.javascript.Application.tasks, routes.javascript.Application.newTask, routes.javascript.Application.doneTask, routes.javascript.Application.undoneTask ) ).as("text/javascript") }
routesに以下を追加。
GET /javascript-routes controllers.Application.javascriptRoutes
そして、テンプレートに以下を追加。
<script src="@routes.Application.javascriptRoutes" type="text/javascript"></script>
で、こんな感じで使う。
jsRouter.controllers.Application.doneTask(id).ajax({ data: { //... }, success: function(data) { // ... }, error: function() { // ... } });
jsRouterをAngularJSで使う時の注意点
jsRouterを使ってデータの取得や更新をやろうとして、最初正常に反映されなくてハマった。最初、以下のようなコードで変更を反映しようとした。
function TodoCtrl($scope) { // ... $scope.addTodo = function() { jsRouter.controllers.Application.newTask().ajax({ data: { label: $scope.todoText }, success: function(data) { $scope.todos.push(data); $scope.todoText = ''; }, error: function() { alert("error:addTask"); } }); }; // ... }
通常AngularJSでajaxするときは$httpという専用のやつを使うのが推奨されているのだけど、jsRouterを使うとjQuery.ajaxを使って通信することになる。で、非同期で通信するとコールバック関数からの値の変更がAngularJSに検出されないようで、画面に変更が反映されない。
これを解決するための1つ目の方法はasync=falseとして同期通信にすること。でも全部を同期通信にしてしまうのはあまりにいけてない。で、もう1つの方法が、変更を通知する関数を使うこと。$scope.$applyという関数を使ってsuccessの処理を以下のように書き換える。
success: function(data) { $scope.$apply(function(){ $scope.todos.push(data); $scope.todoText = ''; }); },
感想
AngularJSっていうか、Javascript MVCフレームワークは使ったことなかったんだけど、なかなか便利。パフォーマンスについてどうなのか気になる所だけど、何かで使ってみたい。画面遷移を最小限にして、サーバサイドの処理のほとんどをJSONを返すAPIを実装していく形にできるのも良い。
AngularJSと組み合わせるときにajax通信をjavascriptRouterを使うべきか、AngularJS標準のでやるかは微妙なところ。AngularJSと使う以外の用途だったら普通に使えそう。
追記
Play2.1が出たので、2.1対応をやってみた。
*1:) => getSession(new H2Adapter, app
SolrのTokenFilterを書いてみる(Scalaで)
Solrのフィルタやらを書けるようになっておきたいよねー、でもJavaは色々面倒なので最近はできれば直接は触りたくないよねー、ということでScalaで書いてみた。Solrの拡張は全然わかってないので色々間違っているかもしれないけれども。サンプルとして書いたコードは、とりあえずGitHubにアップしておいた。
Step1. build.sbtを作成
今回はこんな感じで書いてみた。
name := "sample filters" organization := "com.github.tarhashi" version := "0.1" scalaVersion := "2.9.1" libraryDependencies ++= Seq( "org.apache.lucene" % "lucene-core" % "3.6.0" , "org.apache.solr" % "solr-core" % "3.6.0" )
Step2. TokenFilterFactory, TokenFilterを実装
TokenFilterFactoryはorg.apache.solr.analysis.TokenFilterFactoryインタフェースを実装する必要がある。特に凝ったことをしなくてもいいときはorg.apache.solr.analysis.BaseTokenFilterFactoryを継承して作成すれば最小限のコードでよさそう。Scalaで書くとこんな感じ。
package com.github.tarhashi.solr.sample import org.apache.lucene.analysis.TokenStream import org.apache.solr.analysis.BaseTokenFilterFactory import org.apache.solr.analysis.TokenFilterFactory import com.github.tarhashi.lucene.sample.SampleFilter class SampleFilterFactory extends BaseTokenFilterFactory { override def create(input: TokenStream) : TokenStream = { new SampleFilter(input) } }
次に、org.apache.lucene.analysis.TokenFilterを継承してTokenFilterの実体を作成。試しに、term textのひらがなをカタカナに置き換えるような処理を書いてみた。
package com.github.tarhashi.lucene.sample import org.apache.lucene.analysis.TokenFilter import org.apache.lucene.analysis.TokenStream import scala.collection.mutable.ArrayBuffer import org.apache.lucene.analysis.tokenattributes.CharTermAttribute import org.apache.lucene.analysis.ja.tokenattributes.ReadingAttribute class SampleFilter(input: TokenStream) extends TokenFilter(input) { private val termAttr = addAttribute(classOf[CharTermAttribute]); private val readingAttr = addAttribute(classOf[ReadingAttribute]); def incrementToken(): Boolean = { if (input.incrementToken) { val str = termAttr.toString termAttr.setEmpty.append(convertHiraganaToKatakana(str)) true } else { false } } /** * ひらがなをカタカナに変換する */ private def convertHiraganaToKatakana(str:String):String = { var sb = new StringBuilder val map = (('ぁ' to 'ん').zip(('ァ' to 'ン'))).toMap for(c
Step3. jarファイルを作成
sbtでjarファイルを作成。
$ sbt package ... [info] Packaging /home/masaki/projects/solr-filter/target/scala-2.9.1/sample-filters_2.9.1-0.1.jar ... [info] Done packaging. [success] Total time: 19 s, completed 2012/07/08 21:28:01
Step4. jarファイルを配置
作成したフィルタをexampleのtext_jaに組み込むために、まずはjarファイルを配置。多分scalaのライブラリのjarファイルも参照できないといけないのでそれも配置。
$ mkdir -p /path/to/solr/example/solr/lib $ cp target/scala-2.9.1/sample-filters_2.9.1-0.1.jar /path/to/solr/example/solr/lib $ cp /path/to/scala-library.jar /path/to/solr/example/solr/lib
Step5. 設定ファイルに追加
まずは配置したjarファイルが見つけられるように設定を追加。
$ cd /path/to/solr/example $ vi solr/conf/solrconfig.xml # 他のlibの設定のあたりに/path/to/solr/example/solr/libの設定を追加 <lib dir="/home/masaki/projects/apache-solr-3.6.0/example/solr/lib" />
次に、text_jaに組み込み。
vi solr/conf/schema.xml # fieldType text_jaの一番下に以下を追加 <filter class="com.github.tarhashi.solr.sample.SampleFilterFactory" />
Step6. Solr起動
$ java -jar start.jar
エラーなく起動したら、http://localhost:8983/solr/admin/analysis.jsp から試してみる。なんとなく動いてそう。最初設定が間違っていてうまく動いてくれなかったけど、結果的にはかなり簡単な設定で動いてくれた。
php-buildでphp-fpmを試してみた
phpenv + php-build を使って 5.3 と 5.4 を共存させつつ php-fpm を使うを参考に、php-buildでphp-fpmを生成するようにして試してみた。
$ cd .php-build/share/php-build/definitions/ $ cp 5.4.4 5.4.4-fpm $ vi 5.4.4-fpm
中身はこんな感じで。configure_optionは--enable-fpm以外は適宜追加。
configure_option "--enable-fpm" install_package "http://www.php.net/distributions/php-5.4.4.tar.bz2" install_pyrus install_xdebug "2.2.0"
この設定を使ってbuild。
$ cd $ php-build 5.4.4-fpm ~/.phpenv/versions/5.4.4-fpm
次に、php-fpmの設定ファイルを作成。php-fpm.conf.defaultを元に作成する。
$ cd .phpenv/versions/5.4.4-fpm/etc $ cp php-fpm.conf.default php-fpm.conf # ポート番号など設定を変更するなら修正 $ vi php-fpm.conf
設定ができたら、php-fpmを起動してみる。
$ cd $ .phpenv/versions/5.4.4-fpm/sbin/php-fpm [01-Jul-2012 00:28:32] WARNING: [pool www] 'user' directive is ignored when FPM is not running as root [01-Jul-2012 00:28:32] WARNING: [pool www] 'group' directive is ignored when FPM is not running as root [01-Jul-2012 00:28:32] NOTICE: fpm is running, pid 27385 [01-Jul-2012 00:28:32] NOTICE: ready to handle connections
なんか警告が出るけど気にしない。次に、nginxに設定を追加して再起動。
$ cd /etc/nginx/sites-available/ $ sudo vi phpsrv $ cd ../sites-enabled/ $ sudo ln -s ../sites-available/phpsrv phpsrv $ sudo /etc/init.d/nginx restart
ファイルの中身はこんな感じ。php-fpmのポートを9001に設定したので9001で。
upstream php54 { server 127.0.0.1:9001; } server { listen 80; server_name phpsrv; index index.php; root /home/masaki/projects/phpsrv; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } location ~ \.php.*$ { fastcgi_split_path_info ^(.+\.php)(.*)$; fastcgi_pass php54; fastcgi_index index.php; include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; } }
これでrootに指定した場所に試しにindex.phpを設置してみると、無事動いた。apacheモジュールとして動作させる時と違って、これなら複数バージョンの共存なんかも無理なく出来そうで、素晴らしい。