ネストしたリソースの扱いの話とか

Rails勉強会@東京の月刊Merbで瀧内さんがサンプルを示しながらMerbの使い方を説明してくれました。
で、そのときにブログのようなものを実例に使っていまして、そのCRUDの設計についてプチ議論になっています。

まとめ

なんかコードを書いてみたりしたらえらい長くなったので、私の考えを簡単に要約すると

  • とりあえずこの前提に興味があるならDHHのプレゼンを見るといいと思います
  • map.resource使うといいよ
  • ビューの事情なんだからAjaxとテンプレート差し替えで何とかするのがいいかと
    • それを簡単にするプラギンが欲しい

という。あと読み返して思ったんですが、ネストしたリソースの扱いについての議論に対して、そのリソースはネストしていない、という主張が来てるのか。それはすれ違うなー、と思いました。

ここから意見いろいろ

データ構造としてはよくあるブログを想起していただければいいんですが、Postが複数のCommentを持つというものです。ActiveRecord的に表現すると以下の感じ。

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

この場合に、Comment生成でエラーが発生した場合のハンドリングが難しいね。という話でした。
で、http://d.hatena.ne.jp/lov2much/20090119/1232357948 で難しくないじゃんという声をいただいたわけですが、その場で難しいね、といっていた人たち(私含む)の前提は http://blog.s21g.com/articles/1229 だったという。


先日話題に上った設計は2006年のDHHのプレゼン「Discovering World of Resources on rails」(資料)でRails使いまでリーチして、その実装であるRails 2.xのリリースによってだいぶ定着して来たのかな、と思ってます。先日のコントローラの設計はこれをその前提で話していたので、そこを共有していない方には不親切な議論だったかな、と反省してます。

それはそれとして。

オイラは、リソース名とコントローラ名には、直接的な相関性はないと認識しているクチなんですが......
hogehoges_controller.rbが在るからといって、hogehoge.rbモデルが存在しなければならない、訳ではないですよね?
逆もそうですし。
「それがRailsのセオリーだ」というのなら、それはそれで仕方ないのかな、とも思いますが、でも、それだと、実質上、モデルとコントローラが分離していない == MVCじゃない、ってことになりかねませんか?

http://d.hatena.ne.jp/lov2much/20090120/1232399512

ということで「それがRailsのセオリーだ」というのは多分そうだと思いますよ。
いまのRailsのセオリーは、『ユーザにとって価値がある単位のエンティティの集まり』をリソースとして扱って、それをCRUDすることで大抵のことができるから、そうしようね、というものになってると思います。コントローラレイヤはCRUDを担うシンプルなものにしていこう、というのが(たぶん)基本的な方向性になっています。
余談ですが、このあたりの話は昨年2月の札幌での話とも(私の中では)つながっています。あと100ブクマ越えしたRuby on Rails Code Quality Checklist抄訳でも似たようなことが書いてあった気がします。

で、コメントが一つのリソースたり得るか問題ですが、それをユーザに提示することが意味があるんならリソースとして扱ったほうがRails的にはキレイかと。ここでいうリソースはRailsのARモデル(=DBのテーブル単位)と同じになることも多いですが、たまに違ったりします。なのでid:lov2muchさんが作ってるアプリでのCommentがアグリゲータに完全に内包され、それ自体がリソースにならなというならそうなんでしょうが*1、とりあえずあの場ではCommentそれ自体もリソースとして扱おうとしていた、という前提は押さえていただかないと話が発散するか、と。あくまで、ネストしたリソースをどう扱うか、という話です。

リクエスト => config/routes.rb => リクエストに対応したアクション => hogehoges_controller.rb#action
が、在るべき姿では?
それに、HTTPメソッドによって処理を変えるのって、なんか、よくある「フォームの内容の有無によって出力を変える」cgiみたいで、それこそ、ダサくないですか?

http://d.hatena.ne.jp/lov2much/20090120/1232402979

うーん、よく訓練されたRails 2.x厨としては「アクション名に動詞が含まれるのってダサくないですか?」なわけです。リソースのURIに対して様々なHTTPメソッドでアクセスしてリソースをCRUDするというのが、RailsがRESTから拝借してきたパターンなわけで。ということでmap.resoucesを使いましょう。

でも、やっぱり、主体がPostリソースなんだったら、has_manyでつないだCommentリソース == Post.commentsは、Postリソースの一部ですよ。

http://d.hatena.ne.jp/lov2much/20090120/1232402979

ここがたぶんもっとも意見が異なるところですが、PostはCommentをあくまで集約するものであって内包するものじゃないと思いますよ。むしろ、PostsControllerでやりたい!!というモチベーションからPostの一部だとおっしゃってるように感じます。それはレールから降りる兆候かもしれないのでご注意。

という長い前書きのあとで

私ならこんな感じかなー。Railsです。親を引いてきて一緒に表示するのは「表示したい」というビューの責務によるものですので、振り分けられたあとのテンプレートで必要に応じてロードすればいいと思います。そうじゃないと(最近はCukeを使ってるからあんまり書いてないとはいえ)assignsで検証したり設定したりする変数が多くなりすぎてspec書くのが辛すぎる。

そういう意味でGET /posts/:idにコメント入力フォームを表示するというのもビューの都合なので、あとはJSのクライアント(=Ajaxつきのフォーム)からリソースをCRUDすればいいと思いますよ。私が設計するとしたらまずはそれで、JSの手数増えすぎたりしたら、現実の都合や力不足(=jQuery難しい、とか)で設計を曲げてるよなーというのを意識しながら変更すると思います。

class CommentsController < ApplicationController
  module 誰かこんな感じの無名モジュールを生成するプラギンを作って
    def aggregator(pars = params)
      if pars[:post_id]
        Post.find(pars[:post_id]).comments
      elsif pars[:photo_id]
        Photo.find(pars[:photo_id]).comments
      else
        Comment # or raise
      end
    end

    def aggregator_path(options = {})
      if pars[:post_id]
        post_path( Post.find(pars[:post_id]) )
      ...
    end

    class NestedTemplateFinder < ActionView::TemplateFinder
      def find_base_path_for(template_file_name)
        pars = @controller.params
        if pars[:post_id]
          super("post_" + template_file_name) || super
        elsif pars[:photo_id]
          super("photo_" + template_file_name) || super
        else
          super
        end
      end
    end
  end
  include 誰かこんな感じの無名モジュールを生成するプラギンを作って

  def create
    if @c = aggregator.create(params[:comment])
      redirect_to aggregator_path
    else
      respond_to do |format|
        format.html{ render :action => "show" }
        # でもやっぱりxhrだよねー
        format.js { render :json => @c.errors, :staatus => :unprocessable_entity }
      end
    end
  end
end

*1:その設計に賛成はしませんけど