fields_forを使って関連を削除する方法


fields_forを使って、関連先をチェックボックスで紐付けていたのですが、関連を切りたい場合に困ってしまいました。チェックボックスを外して保存してもうまく削除されずゴミのデータが残ってしまったりしたためです。
これを解決するための方法を見つけたので、メモとして残しておきます。

# サンプル

では、サンプルのコードを書きます。

## Before

### Controller

```rb:OrdersController
class OrdersController < ApplicationController
  before_action :set_order, only: [:show, :edit, :update, :destroy]

  def new
    @order = Order.new
    Item.all.each do |item|
      @order.order_items.build(item: item)
    end
  end

  def edit
    Item.all.each do |item|
      @order.order_items.build(item: item) unless @order.items.exists?(item)
    end
  end

  private
    def set_order
      @order = Order.find(params[:id])
    end

    def order_params
      params.require(:order).permit(:title, order_items_attributes: [:id, :item_id])
    end
end
```

### Model

Orderモデルは複数のItemを持ちます。

```rb:Orderモデル
class Order < ActiveRecord::Base
  has_many :order_items
  has_many :items, through: :order_items
  
  accepts_nested_attributes_for :items
end
```

Orderに持たれるItemモデルです。

```rb:Itemモデル
class Item < ActiveRecord::Base
  has_many :order_items
end
```

OrderとItemを取り持つ中間テーブルを設けます。

```rb:OrderItemモデル
class OrderItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :item
end
```


### View

viewにはfields_forの部分を追加しました。
check_boxにはitemのidをvalueとして持たせます。
チェックが入らない場合はhiddenに設定された0が渡ってきます。

```erb:views/orders/_form.html.erb
<%= form_for(@order) do |f| %>
  <% if @order.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>

      <ul>
      <% @order.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </div>
  <div class="field">
    <label>アイテム</label><br>
    <%= f.fields_for :order_items do |fo| %>
        <label>
          <%= fo.hidden_field :id %>
          <%= fo.check_box :item_id, {checked: fo.object.persisted?}, fo.object.item_id %>
          <%= fo.object.item.name %>
        </label>
    <% end%>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
```

この状態でPOSTすると、チェックが入ってない場合はitem_idが0の状態で中間テーブルに登録されてしまいます。行自体は削除されません。

## After

（追記）mark_for_destructionを使うパターンの場合、リレーション先のバリデーションで、`presense: true`になっていた場合、item_idに0が渡った状態だと弾かれてエラーになってしまうことがわかったので、やっぱり_destroyを使うように変更しました。

### model

変更はOrderモデルです。accepts_nested_attributes_forに`allow_destroy`を追加します。

```rb:Orderモデル
class Order < ActiveRecord::Base
  has_many :order_items
  has_many :items, through: :order_items
  
  accepts_nested_attributes_for :items, allow_destroy: true
end
```

### View

Viewでは、check_boxで_destroyをつけます。チェックがついているときの値を0、ついてない場合を1とすることで、チェックがない場合は削除フラグが付くというふうにしておきます。

```erb:views/orders/_form.html.erb
<%= form_for(@order) do |f| %>
  <% if @order.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>

      <ul>
      <% @order.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </div>
  <div class="field">
    <label>アイテム</label><br>
    <%= f.fields_for :order_items do |fo| %>
        <label>
          <%= fo.hidden_field :id %>
          <%= fo.hidden_field :item_id %>
          <%= fo.check_box :_destroy, {checked: fo.object.persisted?}, 0, 1 %>
          <%= fo.object.item.name %>
        </label>
    <% end%>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
```

こうすることで、関連は綺麗に削除されました。

## 参考URL

* [Active Record Nested Attributes](http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html)
* [Deleting children with accepts_nested_attributes_for in Rails](http://manas.com.ar/blog/2010/03/15/deleting-children-with-accepts_nested_attributes_for-in-rails.html)