|
|
|
|
Rails には各アクションの前後に実行する処理を定義する Filter 機能がある。しかし、よりメタなプログラミングや究極的にDRYを目指していくと、CとVの間でも共通の処理を実行したい局面が出てくる。つまり、アクション(コントローラのメソッド)以上、レンダー(ビューの描画)未満。そんな、友達以上恋人未満的な処理を記述できてこそ、痒い所に手が届くフレームワークたりえるのだ!
Filter機能の現状、および、その理想系をまとめてみる。やはり、CとVを独立したフレームワークだと捉え、それぞれの前後にフィルタ処理を定義したくなるのが人情である。
| 現状 | 理想 |
|---|---|
| before_filter | before_action |
| (コントローラ処理) | (コントローラ処理) |
| after_action | |
| before_render | |
| (描画実行) | (描画実行) |
| after_filter | after_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")を実行するだけでよければ、ApplicationController あたりで perfom_action を再定義してあげればよい。
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 機能はオーバライドとは激しく相性が悪い。なぜなら、
という問題を抱えており、できれば他の手段を利用したいのだが、今のところよい代替手段がないのも事実である。でも、なんとなく Rails2.2 ぐらいの ActiveSupport なんかがあっさり解決してくれそうな気もする。とは言え、現状では本体に直接パッチを当てる以外には上記のようにするしか方法がないので、嫌なことはさっさと忘れて以下のようなコードを楽しむことにしよう。
class MaihaController
protected
def before_render
view = action_name.gsub(/^confirm_/, '')
render :action=>view
end
end |
これは、"confirm_create" と "create" といった共通のビューテンプレートファイルを利用する場合に、それぞれのアクション中に記述するのを避けたコード。他にも flash の値をアクションの実行内容に応じて変更、クリアしたりできて、人生幸せ。(by ぴーちっち)。また、コントローラを継承した場合に各アクションの名前にちょっと変更したビューを使いたい!なんて局面にも激しく有用である。
簡単な "before_render" であれば上記のコードで十分であるが、before_filter 等の既存のフィルタとの親和性や :only, :except を考慮して本物のフィルタにしてあげたい所。でも面倒だ。ということで、例によって RailsChat で moriq 氏を騙して作ってもらった。ありがとうございまっする!(声・須藤茉麻)。
http://dev.moriq.com/svn/rails/plugins/trunk/perform_filters (TODO: 無許可。ヤバかったら消す)
例えば、RJSを使ってると以下の問題に気付く。
一般的にこの問題を解決するには、アクション実行中には実際にはレンダ処理を実行せずに処理として蓄えていって、最後にまとめてレンダ処理として実行する、ことになるが、その「最後」の部分が現在の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 はファイル配置・レイアウト・パフォーマンスなどの問題を抱えている。
| 描画手段 | 問題点 | 解決策 |
|---|---|---|
| action | DoubleRenderError | render_to_string でゴリゴリ |
| component | ファイル配置、レイアウト、パフォーマンス | 我慢 |
| rjs | DoubleRenderError | perform_filters |
rjs はページ更新箇所と更新内容の両方の表現力を持つため、上記のような非常にシンプルで無理がなく、可読性の高いコードを記述可能である。さらに、perform_filters と併用することで、アクションやコントローラの責務がより複雑になった場合にも対応できる。個々のアクションの関係を疎に保ち、それらを繋ぎ合わせるのが perform_filters の仕事なのである。
| 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 | ||||