Posted over 5 years ago. Visible to the public. Repeats.

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

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

Copy
class Invoice 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

Copy
class Invoice 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

Copy
it 'should ignore 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' } } ) invoice.amount.to_s.should == BigDecimal.new("21.0").to_s end

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Author of this card:

Avatar
Martin Straub
Keywords:
howto
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Martin Straub to makandropedia