ちょうどタイミング良く@maihaさんから、@yuguiさんが 近くに来てるという情報をもらったので、 @yamazさんも交えて、ネストしたリソースを扱うコントローラの問題の答えを得るべく、ミーティングをしました。

問題の定義:

モデル層で Post has_many Comment な関係にある時に、 Commentのリストとコメント投稿フォームを含むPosts#show画面(典型的な例としてはブログの一記事表示画面)から、Commentを投稿した場合に、

  1. Comments#createで受け取ると、Commentのsaveに失敗した時に、Post#showを表示したいが、redirect resource(@comment.post) すると、@comment.errorsの情報がロストしてしまう。
  2. Posts#create_commentなどで受け取ると、Commentリソースの処理をPostsコントローラで行う事になって責任の範囲が不明確になり、格好が悪い。

解決策

Merbベースでコンセプトを示します。 まずは、Postsコントローラの中で、以下のような包含関係を宣言するようにします。

   1  class Posts < Application
   2    has_many :comments
   3  end
   4  
   5  class Admin < Application
   6    has_many :comments
   7  end

これにより、コントローラ同士の協調関係を、コントローラ自身が知っているという事になります。 具体的にhas_manyがやることは、以下のようなbeforeフィルターをshowアクションに対してセットする事です。

   1  class Posts < Application
   2    before :only => :show do
   3      controller = Comment.new(request)
   4      @comment = case request.method
   5      when "POST"; controller._dispatch(:create)
   6      when "PUT"; controller._dispatch(:update)
   7      when "DELETE"; controller._dispatch(:destroy)
   8      end
   9    end

:showアクションに対して、本来は使われない"POST", "PUT", "DELETE" の各メソッドでのリクエストがあった場合に、Commentsコントローラに処理を回します。"GET"の場合は普通にPosts#showが行われます。 Commentsコントローラ側では、メソッドの返り値としてcommentオブジェクトを返します。

   1  def create(comment)
   2    Comment.create(comment)
   3  end

作成に失敗した場合は、@comment.errorsにエラー情報が入っているので、 Posts#showの中から利用出来ます。

Posts#show内のComment投稿フォームは以下のような感じで、 Post#showに対してサブミットします。

   1  <%= form_for @comment, :action => resource(@post) do %>
   2    <%= partial "comments/form" %>
   3  <% end =%>

コンセプトなので実際に動くかどうかまだ検証してないですが、 こんな感じで良いのではないかと。

MerbだとControllerがresponseの生成を担当していないので、 コントローラをまたいだ処理のネストが高速に行えます。 Railsの場合は、個々のアクションの実行がresponseの生成を伴うので、 この方法だとオーバヘッドが大きくて難しいかもしれません。

posted by Png genki on Tue 20 Jan 2009 at 02:02

蛇足感がありますが、ちょっと補足しておきます。

Rails勉強会@東京、面白かったんですが。

そもそも何が問題かというと、 Commentsコントローラが担当すべきCommentリソースの処理を、 Postsコントローラで書かなきゃいけないのが格好わるいのでなんとかしたい、という事なんです。

Child belongs_to Parent な関係にあるChildリソースを描画する時は、 多くの場合Parentの描画を伴う事になるので、 そもそもControllerの仕組みが入れ子関係を上手く扱えるようになっているとありがたいのだけど。

View上で階層関係になっているものを、フラットなControllerで処理するという事がそもそも無理があるのかもしれない。 Controller側も階層化させるか、さもなくばセッション中に言ったように、 子供のリソースはAjaxで処理するという形にするのが奇麗かな。

posted by Png genki on Mon 19 Jan 2009 at 20:44

Merbでは、URLとアクションのマッチングをconfig/router.rbの中で定義しますが、ちょっと複雑なパターンマッチングを行いたい場合は、以下のように正規表現を使ってRouteを定義する事ができます。

   1  match(%r{^/gems/(.*)$}).to(
   2    :controller => 'gems', :action => 'show', :name => "[1]")

正規表現にマッチしたグループを、パラメータ側から"[1]"のように後方参照する事ができます。

posted by Png genki on Mon 19 Jan 2009 at 10:56

capistranoはwebアプリケーションをデプロイするための非常に便利なツールですが、出力のログがちょっと読みにくく、どのタスクが実行されたのかが分かりにくいな、と感じていました。 そこで、 capistrano_colors というGemを使って、capistranoの出力を色づけしてみる事にしました。 これを使うと、以下のように分かりやすい感じになります。

ss

設定方法ですが、まずはGemをインストールします。

   1  % sudo gem install capistrano_colors

続いて、config/deploy.rb (いわゆるレシピファイル)の中か、 ~/.caprcの中で、以下の一行を追加します。

   1  require 'capistrano_colors'

これでOKです。あとは、普通にcapコマンドを使えば、 色づけされた出力が得られます。

posted by Png genki on Sun 18 Jan 2009 at 02:30

#githubで質問したら、この記事を紹介してくれたので読んでみました。

Gem Rebuilds only on Version Bump

We recently changed the system so that only gemspec pushes that contain a bumped version will be built. This will prevent accidental gem clobbering and we can now guarantee that when you release a specific gem version, that version will never change.

昔はgemspecファイルがちょっとでも編集されていれば良かったのですが、 1/13の時点で、GitHub上でGemを再生成するためには、バージョン番号を増やさないと駄目になっているようです。

僕は、コメント欄でhalorgiumさんが言っているように、 "next-to-be-released"アプローチを支持したいのですが、どうなりますかね。 Merbや最近のRailsのように、ライブラリが全てpackage化されたGemの形で提供される事が想定されている場合、 いままではGitHubでEdgeGemを作ってそれを使っていたのですが、 これからはバージョン番号を上げない限り、それが出来なくなってしまいます。

とりあえずは、4番目のリビジョン番号を機械的にインクリメントする事で対応しようかなと考えていますが、もっと良い方法はないものかな。

他に気になった話題として、technomancyさんがコメントで書いている事によると、次のバージョンのRubygemsからは、"1.1.0.RC1"のような プリリリースバージョンである事を示す文字列を認識するようになるそうです。 確かにこうすれば"next-to-be-released"的な使い方も出来るので良いかな。 しかし、version文字列が数字とドットだけであるという想定に依存してるソフトウェアが、しばらくエラーを出すようになる気もしますね。

posted by Png genki on Sun 18 Jan 2009 at 00:20

Mattettiさんから連絡があって、 forkして開発していたmerb_babel本家に取り込んでもらいました。 以下のような機能を追加しています。

  • Merb::Requestからの国コード判別機能
  • YAMLを利用した階層化ローカライゼーション
  • 時刻のローカライゼーション

将来的には、merb-sliceの形にして、ローカライズファイルの オンライン編集が出来るようにしたい。

posted by Png genki on Sat 17 Jan 2009 at 04:54

Merbのプラグインを作る場合、merb-gen plugin plugin-name でひな形が生成されますが、現状では生成されるspecがほとんど空っぽなので、 ちゃんとしたspecを書くための足場の作り方を紹介します。

まずは、spec/spec_helper.rb を以下のような感じに準備します (これはdm-has-versionsの例です)

   1  $:.push File.join(File.dirname(__FILE__), '..', 'lib')
   2  
   3  require 'rubygems'
   4  require 'merb-core'
   5  require 'dm-core'
   6  require "spec"
   7  require 'dm-has-versions/has/versions'
   8  require 'dm-aggregates'
   9  
  10  DataMapper::Model.append_extensions DataMapper::Has::Versions
  11  Merb.disable(:initfile)
  12  Merb.start_environment(
  13    :testing      => true,
  14    :adapter      => 'runner',
  15    :environment  => ENV['MERB_ENV'] || 'test',
  16    :merb_root    => File.dirname(__FILE__) / 'fixture',
  17    :log_file     => File.dirname(__FILE__) / "merb_test.log"
  18  )
  19  DataMapper.setup(:default, "sqlite3::memory:")
  20  
  21  Spec::Runner.configure do |config|
  22    config.include(Merb::Test::ViewHelper)
  23    config.include(Merb::Test::RouteHelper)
  24    config.include(Merb::Test::ControllerHelper)
  25  
  26    DataMapper.auto_migrate!
  27  end

この例では、DataMapperを使う事を前提としています。 "sqlite3::memory:" を指定することで、テストのための データベースファイルなどを用意する必要がないので楽です。

テストで利用されるクラス群は、spec/fixture 以下に、 通常のMerbアプリケーションと同様のディレクトリ階層で用意します。

   1  % tree spec/fixture [~/project/dm-has-versions:master]
   2  spec/fixture
   3  `-- app
   4      `-- models
   5          |-- comment.rb
   6          `-- story.rb

posted by Png genki on Fri 16 Jan 2009 at 13:35

DataMapper用のバージョン管理プラグイン、 dm-has-versions をリリースしました。

dm-is-versionedというライブラリが既にあるのですが、Railsで慣れ親しんだacts_as_versionedと微妙に挙動が違うのと、revert_toやversion=ができないなど、細かいところが足りない感じがしたので作りました。

USAGE:

以下のコードをご覧の通りです。

   1  class Story
   2    include DataMapper::Resource
   3          
   4    property :id, Integer, :serial => true
   5    property :title, String
   6    property :updated_at, DateTime
   7  
   8    has_versions :ignore => [:updated_at]
   9  end
  10  
  11  Story.auto_upgrade!
  12  
  13  story = Story.create(:title => 'hello')
  14  story.version #=> 0
  15  story.update_attributes :title => 'good night'
  16  story.version #=> 1
  17  story.title #=> 'good night'
  18  story.version = 0
  19  story.title #=> 'hello'

auto_upgrade!は最初に一回だけ必要です。

posted by Png genki on Fri 16 Jan 2009 at 03:42

Merbの主要な開発者の一人であるwycats氏のgithub上のリポジトリに、 Rails3の元となるかもしれないコードがコミットされているようです。

ss

見慣れないciというディレクトリは、Continuous Integrationではないか(maiha談)とのこと。

Update

ActiveORM なるものを見つけました。これは予想通り、AR, DM, Sequelなどの共通基底となる何かでしょうか?

   1  module ActiveORM
   2    autoload :VERSION, 'active_orm/version'
   3    autoload :Core, 'active_orm/core'
   4    
   5    module Proxies
   6      autoload :AbstractProxy, 'active_orm/proxies/abstract_proxy'
   7      autoload :DataMapperProxy, 'active_orm/proxies/active_record_proxy'
   8      autoload :DataMapperProxy, 'active_orm/proxies/datamapper_proxy'
   9      autoload :SequelProxy, 'active_orm/proxies/sequel_proxy'
  10    end
  11    
  12    class << self
  13      include Core::ClassMethods
  14    end
  15  end

これはBINGOっぽい。

See Also

posted by Png genki on Thu 15 Jan 2009 at 09:54

dm-is-paginated-0.0.1は、specが走らない状態だったので、 Railsの pagination_scope風味のDataMapper用Paginationライブラリとして、 dm-pagination を作りました。

   1  class Posts
   2    def index
   3      @posts = Post.paginate(:page => params[:page])
   4    end

上記のようにコントローラでpaginationオブジェクトを作成し、 Viewから以下のように参照します。

   1  <ul>
   2  <% @posts.each do |post| %>
   3    <li><%= h(post.body) %></li>
   4  <% end %>
   5  </ul>
   6  <%= paginate @posts %>

posted by Png genki on Thu 15 Jan 2009 at 00:41