Rails has_many Association: collection = не работает должным образом

В счете-фактуре много записей:

class Invoice < ActiveRecord::Base
  has_many :invoice_entries, :autosave => true, :dependent => :destroy
  validates_presence_of :date
end

class InvoiceEntry < ActiveRecord::Base
  belongs_to :invoice
  validates_presence_of :description
end

Предположим, у нас есть один счет в базе данных:

id: 1
date: '2013-06-16'

и в нем есть две записи в счетах:

id: 10                           id: 11
invoice_id: 1                    invoice_id: 1
description: 'do A'              description: 'do C'

Теперь у меня есть новые записи в счетах:

id: 10                               
description: 'do B'              description: 'do D'

(Existing invoice entry          (New invoice entry
 with updated description)        without id)

Я хочу, чтобы в счете-фактуре были только эти новые записи счета-фактуры (это означает, что запись счета-фактуры с id=11 должна быть удалена).

invoice.invoice_entries = new_invoice_entries, кажется, делает половину работы. Он удаляет запись счета-фактуры с id=11, создает новую запись счета-фактуры с описанием 'Do D', но не обновляет описание записи счета-фактуры с id=10 с 'Do A' на 'Do B'. Я предполагаю, что когда Rails видит существующий id в new_invoice_entries, он полностью игнорирует его. Это правда? Если да, то в чем причина этого?

Мой полный код ниже. Как бы вы решили эту проблему? (Я использую Rails 4, на случай, если он упрощает код.)


# PATCH/PUT /api/invoices/5
def update
  @invoice = Invoice.find(params[:id])
  errors = []

  # Invoice entries
  invoice_entries_params = params[:invoice_entries] || []
  invoice_entries = []

  for invoice_entry_params in invoice_entries_params
    if invoice_entry_params[:id].nil?
      invoice_entry = InvoiceEntry.new(invoice_entry_params)
      errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
    else
      invoice_entry = InvoiceEntry.find_by_id(invoice_entry_params[:id])

      if invoice_entry.nil?
        errors << "Couldn't find invoice entry with id = #{invoice_entry_params[:id]}"
      else
        invoice_entry.assign_attributes(invoice_entry_params)
        errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
      end
    end

    invoice_entries << invoice_entry
  end

  # Invoice
  @invoice.assign_attributes(date: params[:date])

  errors << @invoice.errors.messages.values if not @invoice.valid?

  if errors.empty?
    # Save everything
    @invoice.invoice_entries = invoice_entries
    @invoice.save

    head :no_content
  else
    render json: errors.flatten, status: :unprocessable_entity
  end
end

person Misha Moroshko    schedule 16.06.2013    source источник


Ответы (2)


Чтобы изменить не только ассоциацию, но и атрибуты связанных объектов, вы должны использовать accepts_nested_attributes_for:

class Invoice < ActiveRecord::Base
  has_many :invoice_entries, :autosave => true, :dependent => :destroy
  validates_presence_of :date
  accepts_nested_attributes_for :invoice_entries, allow_destroy: true
end

Есть серия 196 railscast о том, как создавать динамические вложенные формы с помощью nested_attributes.

Дополнение:

accepts_nested_attributes_for ожидает атрибуты для вложенных моделей во вложенном хэше, то есть:

invoice_params={"date" => '2013-06-16', 
  "invoice_entries_attributes" => [
    {"description" => "do A"},
    {"description" => "do B"}]
}

invoice= Invoice.new(invoice_params)
invoice.save

save сохраняет invoice и два invoice_items.

Теперь

invoice=Invoice.find(1)
invoice_params={
  "invoice_entries_attributes" => [
    {"description" => "do A"},
    {"description" => "do C"}]
}
invoice.update_attributes(invoice_params)

удаляет элемент do B и добавляет элемент do C.

form_fields можно использовать для создания форм, в результате которых получаются именно такие вложенные хэши.
Подробнее см. Railscast.

person Martin M    schedule 16.06.2013
comment
Я пробовал добавить accepts_nested_attributes_for :invoice_entries, но без разницы. См. Также: stackoverflow.com/q/17142290/247243 - person Misha Moroshko; 17.06.2013

Попробуйте использовать accepts_nested_attributes_for. Это очистит большую часть вашего кода! Вот пример:

class Invoice < ActiveRecord::Base
  has_many :invoice_entries, :dependent => :destroy
  validates_presence_of :date
  attr_accessible :invoice_entries_attributes

  accepts_nested_attributes_for :invoice_entries, :allow_destroy => true
end

В представлении вы можете затем использовать fields_for (simple_fields_for с простой формой и semantic_fields_for с formtastic, если вы используете один из этих драгоценных камней).

<%= form_for @invoice do |invoice_form| %>
  <%= invoice_form.fields_for :invoice_entries do |invoice_entry_form| %>
    <%= invoice_entry_form.text_field :description %>
    <%= invoice_entry_form.check_box :_destroy %>
  <% end %>
<% end %>

Теперь в вашем контроллере вы можете провести рефакторинг до основ:

# PATCH/PUT /api/invoices/5
def update
  @invoice = Invoice.find(params[:id])
  if @invoice.update_attributes(params[:invoice]) # This also saves all associated invoice entries, and destroy all that is marked for destruction.
    head :no_content
  else
    render json: @invoice.errors.flatten, status: :unprocessable_entity
  end
end

Вы можете узнать больше о accepts_nested_attributes_for здесь: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Или вы можете посмотреть этот обзор вложенных моделей: http://railscasts.com/episodes/196-nested-model-form-revised

person jokklan    schedule 16.06.2013
comment
Добавление accepts_nested_attributes_for :invoice_entries не помогает. См. Также: stackoverflow.com/q/17142290/247243 - person Misha Moroshko; 17.06.2013