2.0の:_methodには:_methodで対抗!

以前から悩んでいた、link_to_remoteでリクエストがPUTメソッドとして扱われてしまい、常にupdateアクション呼び出しになってしまう現象への対策の続き...

PUT問題の始まり

  • 以下のような環境で、link_to_remoteのリンクをクリックすると、挿入・削除・コピーとも常にupdateアクションが実行されてしまい、困っていた...。
# ルート設定: config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :slips
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end
# ビュー: app/views/slips/edit.html.erb
<div class="slip" id="slip">
<h1><%= _('Editing slip') %></h1>

<% form_for @slip do |f| %>
...(中略)...
  <%= link_to_remote "1行挿入", :submit=>'slip', :url=>{:action=>'insert_row', :index=>form.index} %>
  <%= link_to_remote "1行削除", :submit=>'slip', :url=>{:action=>'delete_row', :index=>form.index} %>
  <%= link_to_remote "1行コピー", :submit=>'slip', :url=>{:action=>'copy_row', :index=>form.index} %>
...(中略)...
<% end %>
</div>
  • 原因はform_forが見えないinputタグを生成して、:_method=>"put"を送信しているためと思われるが、それではどうすればよいか?
# ビュー: app/views/slips/edit.html.erb
<div class="slip" id="slip">
<h1><%= _('Editing slip') %></h1>

<% form_for @slip do |f| %><div style="margin:0;padding:0"><input name="_method" type="hidden" value="put" /></div>
...(中略)...
  <%= link_to_remote "1行挿入", :submit=>'slip', :url=>{:action=>'insert_row', :index=>form.index} %>
  <%= link_to_remote "1行削除", :submit=>'slip', :url=>{:action=>'delete_row', :index=>form.index} %>
  <%= link_to_remote "1行コピー", :submit=>'slip', :url=>{:action=>'copy_row', :index=>form.index} %>
...(中略)...
<% end %>
</div>

これまで以下の対策を考えていたが、それぞれ問題があった...。

  • :submitの送信範囲を限定して、<input name="_method" type="hidden" value="put" />を送信しない範囲を設定する。
    • →form_forを含んだ範囲の送信が必須の状況ではどうしようもない...。
  • ルート設定に:member=>[:insert_row, :delete_row, :copy_row]オプションを追加する。
    • →RESTなHTTPメソッドの使い方を無視している...。(:delete_rowアクションをPUTで実行することになってしまう。)


悩んでいるうちに、さらに疑問は深まり...

  • 「:_method=>'put'が送信されるのは、submitボタンを押した時だけにして欲しいものだ。」
    • →link_to_remoteの:submitの範囲にform_forが含まれている場合、<input name="_method" type="hidden" value="put" />の送信を止めることは出来ない。
    • →よって、link_to_remoteの時:_method=>"put"を送信したくないのであれば、:submitの範囲指定を、:_method=>"put"を含まない範囲に限定するしかない...。
  • 「それとも:_method=>'put'が渡されても、POSTメソッドが優先される方法ってあるのだろうか?」
    • →未解決...。


そして、最近閃いた。(詳細は以下)

:_methodには:_methodで対抗する

  • :method=>:postオプションを指定してみたが、それでもRailsはPUTと解釈してしまう。:_methodパラメーターが優先されているようだ...。
  <%= link_to_remote "1行挿入", :submit=>'slip', :url=>{:action=>'insert_row', :index=>form.index}, :method=>:post %>
  <%= link_to_remote "1行削除", :submit=>'slip', :url=>{:action=>'delete_row', :index=>form.index}, :method=>:post %>
  <%= link_to_remote "1行コピー", :submit=>'slip', :url=>{:action=>'copy_row', :index=>form.index}, :method=>:post %>
  • そうか!それなら:_methodパラメーターを、urlパラメーターとして再設定してみる。
  <%= link_to_remote "1行挿入", :submit=>'slip', :url=>{:action=>'insert_row', :index=>form.index, :_method=>:post}, :method=>:post %>
  <%= link_to_remote "1行削除", :submit=>'slip', :url=>{:action=>'delete_row', :index=>form.index, :_method=>:post}, :method=>:post %>
  <%= link_to_remote "1行コピー", :submit=>'slip', :url=>{:action=>'copy_row', :index=>form.index, :_method=>:post}, :method=>:post %>
  • うまくいった!これでeditページでも挿入・削除・コピーがちゃんと動くようになった!(今までなぜこの方法が思い付かなかったのか...。)
  • ただし、Railsでは、urlパラメーターはformパラメーターよりも優先されるという経験則に基づいている。(正しい認識だろうか?)

REST風に...

せっかく、map.resourcesのオプションを覚えたので、さっそく利用してみる。

  • :collectionオプション設定を追記する。
# ルート設定: config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :slips, :collection=>{:insert_row=>:post, :copy_row=>:post, :delete_row=>:delete}
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end
  • rake routesで確認してみる。オレンジ色の部分が:collectionオプションによって追加された。
          insert_row_slips POST   /slips/insert_row                {:controller=>"slips", :action=>"insert_row"}
formatted_insert_row_slips POST   /slips/insert_row.:format        {:controller=>"slips", :action=>"insert_row"}
            copy_row_slips POST   /slips/copy_row                  {:controller=>"slips", :action=>"copy_row"}
  formatted_copy_row_slips POST   /slips/copy_row.:format          {:controller=>"slips", :action=>"copy_row"}
          delete_row_slips DELETE /slips/delete_row                {:controller=>"slips", :action=>"delete_row"}
formatted_delete_row_slips DELETE /slips/delete_row.:format        {:controller=>"slips", :action=>"delete_row"}
                     slips GET    /slips                           {:controller=>"slips", :action=>"index"}
           formatted_slips GET    /slips.:format                   {:controller=>"slips", :action=>"index"}
                           POST   /slips                           {:controller=>"slips", :action=>"create"}
                           POST   /slips.:format                   {:controller=>"slips", :action=>"create"}
                  new_slip GET    /slips/new                       {:controller=>"slips", :action=>"new"}
        formatted_new_slip GET    /slips/new.:format               {:controller=>"slips", :action=>"new"}
                 edit_slip GET    /slips/:id/edit                  {:controller=>"slips", :action=>"edit"}
       formatted_edit_slip GET    /slips/:id/edit.:format          {:controller=>"slips", :action=>"edit"}
                      slip GET    /slips/:id                       {:controller=>"slips", :action=>"show"}
            formatted_slip GET    /slips/:id.:format               {:controller=>"slips", :action=>"show"}
                           PUT    /slips/:id                       {:controller=>"slips", :action=>"update"}
                           PUT    /slips/:id.:format               {:controller=>"slips", :action=>"update"}
                           DELETE /slips/:id                       {:controller=>"slips", :action=>"destroy"}
                           DELETE /slips/:id.:format               {:controller=>"slips", :action=>"destroy"}
                                  /:controller/:action/:id         
                                  /:controller/:action/:id.:format 
  • RESTな名前付きルートも利用してみる。
# ビュー: app/views/slips/edit.html.erb
<div class="slip" id="slip">
<h1><%= _('Editing slip') %></h1>

<% form_for @slip do |f| %>
...(中略)...
  <%= link_to_remote "1行挿入", :submit=>'slip', :url=>insert_row_slips_path(:index=>form.index, :_method=>:post), 
                     :html=>{:title=>"1行挿入"}, :method=>:post %>
  <%= link_to_remote "1行削除", :submit=>'slip', :url=>delete_row_slips_path(:index=>form.index, :_method=>:delete), 
                     :html=>{:title=>"1行削除"}, :method=>:delete %>
  <%= link_to_remote "1行コピー", :submit=>'slip', :url=>copy_row_slips_path(:index=>form.index, :_method=>:post), 
                     :html=>{:title=>"1行コピー"}, :method=>:post %>
...(中略)...
<% end %>
</div>


これまでのPUT対策で一番気に入った方法なのだが、果たしてこれが良い方法なのかどうか...。悩み続けてもしょうがないので、さらに良い方法が見つかるまではこの作戦でやってみる!

      • Rails 2.1では「insert_row_slips_path(:index=>form.index, :_method=>:post)」オレンジ部分のような回りくどい方法は不要になった。
      • 素直に「:method=>:post」オプションのみでPOSTメソッドとして送信してくれる!