ActiveRecord: When aggregating nested children, always exclude children marked for destruction

Updated . Posted . Visible to the public. Repeats.

When your model is using a callback like before_save or before_validation to calculate an aggregated value from its children, it needs to skip those children that are #marked_for_destruction?. Otherwise you will include children that have been ticked for deletion in a nested form.

Wrong way

class Invoice < ApplicationRecord
  has_many :invoice_items
  accepts_nested_attributes_for :invoice_items, :allow_destroy => true # the critical code 1/2
  before_save :calculate_and_store_amount                              # the critical code 2/2

  private      
  
  def calculate_and_store_amount
    # may include amounts of items you marked for destruction
    self.amount = invoice_items.collect(&:amount).sum
  end
end

Right way

class Invoice < ApplicationRecord
  has_many :invoice_items
  accepts_nested_attributes_for :invoice_items, :allow_destroy => true # the critical code 1/2
  before_save :calculate_and_store_amount                              # the critical code 2/2
  
  private
  
  def calculate_and_store_amount
    # does not include amounts of items you marked for destruction
    self.amount = invoice_items.reject(&:marked_for_destruction?).sum(&:amount)
  end
end

How to test the correct behaviour in rspec

it 'ignores invoice items marked for destruction in a nested update' do
  invoice = Invoice.make
  item1 = InvoiceItem.make(:invoice => invoice, :amount => BigDecimal.new("10.0"))
  item2 = InvoiceItem.make(:invoice => invoice, :amount => BigDecimal.new("21.0"))
  
  invoice.update_attributes!(
    :invoice_items_attributes => {
      '0' => {
        'id' => item1.id,
        '_destroy' => '1'
      }
    }
  )
  
  expect(invoice.amount.to_s).to eq(BigDecimal.new("21.0").to_s)
end
Martin Straub
Last edit
Felix Eschey
Keywords
howto
License
Source code in this card is licensed under the MIT License.
Posted by Martin Straub to makandra dev (2012-02-23 14:45)