2006-10-13 優しいRailsの育て方 [長年日記]

[Rails] before_render

Rails には各アクションの前後に実行する処理を定義する Filter 機能がある。しかし、よりメタなプログラミングや究極的にDRYを目指していくと、CとVの間でも共通の処理を実行したい局面が出てくる。つまり、アクション(コントローラのメソッド)以上、レンダー(ビューの描画)未満。そんな、友達以上恋人未満的な処理を記述できてこそ、痒い所に手が届くフレームワークたりえるのだ!

現状

Filter機能の現状、および、その理想系をまとめてみる。やはり、CとVを独立したフレームワークだと捉え、それぞれの前後にフィルタ処理を定義したくなるのが人情である。

現状理想
before_filterbefore_action
(コントローラ処理)(コントローラ処理)
after_action
before_render
(描画実行)(描画実行)
after_filterafter_render

(※ 本当はコントローラ処理の任意の箇所で render を実行できるので正確ではないが大筋で)

php5 のフレームワークである Canvas(参考2)はまさにこの理想系が実装されているようだ。素晴らしい。Railsはと言うと、表中の "before_action", "after_render" に関しては既存のフィルタ機能で代用できるため、残りは中央の2つになるが、現在のRailsにおけるアクション実行を考えると、CV間に密接な関係がありユーザが介入することができないため、これら "after_action", "before_render" は同じにしても構わないだろう(*1)。

(*1) 理想的には、アクションとレンダーの間にメソッドを用意して、そこでインスタンス変数の assignment 等を行うようにすべきだと思う。(将来的に、ERb以外のビューを使う場合など)。現在のRailsはフレームワークとしては明確にCとVが分かれているが、実際はVはERbを想定したコーディングがなされているという現状も表している。

before_render

よって、そのレンダー前に実行するメソッドが欲しいのだが、フィルタのように複数存在せず、決まったメソッド(例えば "before_render")を実行するだけでよければ、ApplicationController あたりで perfom_action を再定義してあげればよい。

app/controllers/application.rb:

class ApplicationController < ActionController::Base
private
  def perform_action_without_filters
    if self.class.action_methods.include?(action_name) || self.class.action_methods.include?('method_missing')
      send(action_name)
      before_render if respond_to?(:before_render)
      render unless performed?
    elsif template_exists? && template_public?
      render
    else
      raise UnknownAction, "No action responded to #{action_name}", caller
    end
  end
end

(※ 1.1系でのコード。Edge は若干 perform_action の定義が違うので、適当に "before_render" を突っ込む方向で)

ここで注意する点は、"perform_action" でなく "perform_action_without_filters" を上書きすること。その理由は、「filter 関係だから」ではなく、「ActionController#perform_action が最初に機能拡張されてるのが Filter によってだから」である。これは Rails の機能拡張手段が Ruby の alias 機能を使っているせいで、alias 機能はオーバライドとは激しく相性が悪い。なぜなら、

  • 先日の "AR#save_without_validations!" がバグっているのもこのせい
  • 将来的にももしFilterの前に追加される拡張機能ができたりしたら、こういう拡張箇所を全部修正して回る必要がある

という問題を抱えており、できれば他の手段を利用したいのだが、今のところよい代替手段がないのも事実である。でも、なんとなく Rails2.2 ぐらいの ActiveSupport なんかがあっさり解決してくれそうな気もする。とは言え、現状では本体に直接パッチを当てる以外には上記のようにするしか方法がないので、嫌なことはさっさと忘れて以下のようなコードを楽しむことにしよう。

before_render

class MaihaController
protected
  def before_render
    view = action_name.gsub(/^confirm_/, '')
    render :action=>view
  end
end

これは、"confirm_create" と "create" といった共通のビューテンプレートファイルを利用する場合に、それぞれのアクション中に記述するのを避けたコード。他にも flash の値をアクションの実行内容に応じて変更、クリアしたりできて、人生幸せ。(by ぴーちっち)。また、コントローラを継承した場合に各アクションの名前にちょっと変更したビューを使いたい!なんて局面にも激しく有用である。

perform_filters

簡単な "before_render" であれば上記のコードで十分であるが、before_filter 等の既存のフィルタとの親和性や :only, :except を考慮して本物のフィルタにしてあげたい所。でも面倒だ。ということで、例によって RailsChat で moriq 氏を騙して作ってもらった。ありがとうございまっする!(声・須藤茉麻)。

http://dev.moriq.com/svn/rails/plugins/trunk/perform_filters (TODO: 無許可。ヤバかったら消す)

例えば、RJSを使ってると以下の問題に気付く。

  • render :update は一度しか実行できない
  • アクション定義の地理的に離れた場所でRJSによるDOM操作を実行したい

一般的にこの問題を解決するには、アクション実行中には実際にはレンダ処理を実行せずに処理として蓄えていって、最後にまとめてレンダ処理として実行する、ことになるが、その「最後」の部分が現在のRailsには存在しない。そして、その機会を与えてくれるのが "perform_filters" なのである。

例として任意のSQL(SELECT文)を実行する管理用コントローラを考える。入力フォーム(textarea)にSQLを入力して実行するだけであるが、100万件の結果セットになるとマジ死亡すぎるので、まずは入力したクエリを "SELECT COUNT FROM (...)"でラッピングして上位10件のみをサマリ表示することにする。

任意のクエリを実行するコントローラ

class SqlController < ApplicationController
  before_filter  :prepare_query, :except=>"index"
  perform_filter :before_render, :except=>"index"

protected
  ### Filters
  def prepare_query
    !! @query = params[:query].to_s.strip.sub(/;$/, '').should.not.be.blank {nil}
  end

  def before_render
    return unless rjs.blank?
    render :update do |page|
      @rjs.each do |block| block.call(page) end
    end
  end

  ### Accessor Methods
  def rjs(&block)
    block ? rjs << block : @rjs ||= []
  end

public
  def index
  end

  def count
    query  = "SELECT COUNT(*) AS cnt FROM (%s) AS src" % @query
    @total = ActiveRecord::Base.count_by_sql(query)
    rjs do |page|
      page[:message].replace_html "%d件ヒットしました" % @total
      page[:message].visual_effect :highlight
    end
  end

  def select(limit = nil)
    limit    = limit.to_i
    query    = (limit > 0) ? "SELECT * FROM (%s) AS src LIMIT %d" % [@query, limit] : @query
    @records = ActiveRecord::Base.find_by_sql(query)
    rjs do |page|
      page[:records].replace :partial=>"records"
    end
  end

  def summary
    count
    select(10)
  end
end

"count", "select" はそれぞれ、結果セットの個数、結果セット全体を返すアクションで、"summary" はその両者を実行するアクションである。この"summary" のような composite action とも呼ぶべき アクション定義を行う場合、DoubleRenderError に悩まされ、頼みの components はファイル配置・レイアウト・パフォーマンスなどの問題を抱えている。

描画手段問題点解決策
actionDoubleRenderErrorrender_to_string でゴリゴリ
componentファイル配置、レイアウト、パフォーマンス我慢
rjsDoubleRenderErrorperform_filters

rjs はページ更新箇所と更新内容の両方の表現力を持つため、上記のような非常にシンプルで無理がなく、可読性の高いコードを記述可能である。さらに、perform_filters と併用することで、アクションやコントローラの責務がより複雑になった場合にも対応できる。個々のアクションの関係を疎に保ち、それらを繋ぎ合わせるのが perform_filters の仕事なのである。

まとめ

  • rjs + perform_filters は Rails 界の Web2.01
  • perform_filters はメジャーになる前に名前をもう一度考えたい
  • もう "index" だけが ERb のビューで、それ以外のアクションは全て RJS でいいよ

サイト内検索 (by Google)

| JRuby | Rails | Berryz | ℃-ute | エッグ | jQuery |

過去

2006年
10月
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

未来

コンタクト