2.0のPUTにハマる

test_slip202プロジェクトは、Rails1.2.6環境で作り始めた明細行を持つ伝票入力のサンプルプロジェクトだ。1.2.6環境までは調子よく動いている。最近、Rails2.0.2環境にしてから、明細行の挿入・削除・コピーがeditページの時だけ機能しなくなっていることに気付いた。(newページでは正常に動作する。不思議...。)伝票を表示するビューは、おおよそ以下と同等のコードになっている。

  • 以下で伝票の共通情報を描画する。
  • 明細行はテーブルの中でrender :partial=>'journals/form'を呼び出して描画している。
# ビュー: app/views/slips/edit.html.erb
<div class="slip" id="slip">
<h1><%= _('Editing slip') %></h1>

<% form_for @slip do |f| %>
  <%= f.text_field :number  %>
  <%= f.text_field :executed_on  %>
  <%= f.yen_field :total_yen  %>

  <%= f.error_messages_on :base %>
  <table class="journal" id="journal">
    <%= render :partial=>'journals/form', :collection=>@journals %>
  </table>

  <%= f.submit %>
<% end %>

<%= link_to _('Show'), @slip %> |
<%= link_to _('Back'), slips_path %>
</div>
  • 以下で明細行を描画する。
# ビュー: app/views/journals/_form.html.erb
<% fields_for form, :index=>form.index do |j| %>
  <th>
    <%= 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} %>
  </th>
  <th align="right">
    <%= content_tag 'span', form_counter + 1, :id=>"journal_#{form.index}_number", :class=>'journal_number' %>
    <%= j.hidden_field :position, :value=>form_counter + 1, :class=>'journal_position' %>
    <%= j.hidden_field :index %>
  </th>
  <td>
    <%= j.text_field :comment, :size=>40 %>
  </td>
  <td>
    <%= j.yen_field :yen %>
  </td>
<% end %>

常にupdateアクションが呼び出される...

上記コードjournals/_form.html.erbのlink_to_remoteを見ると、今までの経験から、挿入・削除・コピーのリンクがクリックされると、insert_row・delete_row・copy_rowアクションが実行されると予想する。(実際、Rails1.2.6まではそのように動作した。)ところが、Rails2.0.2環境では、常にupdateアクションが呼び出される。不思議...。

リクエストがPUTメソッドで送信される...

調べているうちに、editページでクリックした時は常にPUTメソッド扱いで送信されていることに気付く!考えてみると、editページのurlは、/slips/1/editのようになっている。この状態でPUTメソッドを送信すると、RESTなRails2.0.2環境ではupdateアクションと判断されてしまい、その結果、常にupdateアクションが呼び出される状態になっていたのだ...。

なぜPUTメソッドで送信されるのか?

ブラウザのソース表示で確認すると、Rails2.0.2環境では、form_forは見えないinputタグを生成していた。以下は空のform_forブロックが生成したHTMLの例。@slipが新規か既存かで判定され、既存のレコードの時は:_method=>"put"が送信されることになる。

# ビュー: app/views/slips/new.rhtml

<% form_for(@slip) do |f| %>
<% end %>
<%# 以下のHTMLが生成された。%>

<form action="/slips" class="new_slip" id="new_slip" method="post">
  <div style="margin:0;padding:0">
    <input name="authenticity_token" type="hidden" value="bc70df10a269ed11ebcd411fbebf0b732908f40b" />
  </div>
</form>
# ビュー: app/views/slips/edit.rhtml

<% form_for(@slip) do |f| %>
<% end %>
<%# 以下のHTMLが生成された。%>

<form action="/slips/11" class="edit_slip" id="edit_slip_11" method="post">
  <div style="margin:0;padding:0">
    <input name="_method" type="hidden" value="put" />
    <input name="authenticity_token" type="hidden" value="bc70df10a269ed11ebcd411fbebf0b732908f40b" />
  </div>
</form>

これは、RailsがHTMLの規格に沿ってPUT、DELETEメソッドをサポートしても、現状のブラウザでは実装されていないので、それを補うため、見えないinputタグで:_method=>"put"を送信しているらしい。Railsは:_method=>"put"を受信すると、postであってもputメソッドと解釈して処理を進めるようだ。

送信範囲を限定する

問題は、link_to_remoteのオプション指定:submit=>'slip'にあった。ここで伝票全体を指定する<div id="slip">の範囲を指定しているので、AjaxリクエストまでPUTメソッドとして取り扱われてしまっていたのだ...。送信範囲を限定して以下のようにしてみた。

<% fields_for form, :index=>form.index do |j| %>
  <th>
    <%= link_to_remote "1行挿入", :submit=>'journal', :url=>{:action=>'insert_row', :index=>form.index} %>
    <%= link_to_remote "1行削除", :submit=>'journal', :url=>{:action=>'delete_row', :index=>form.index} %>
    <%= link_to_remote "1行コピー", :submit=>'journal', :url=>{:action=>'copy_row', :index=>form.index} %>
...(中略)...


以上で、送信範囲を<table id="journal">に変更して、無事、挿入・削除・コピーが機能するようになった!