Hi... I'm trying to use nested models in a situation where I've been using :touch => true to update a parent model, resulting in an infinite loop that quickly blows the stack. Here's a simple relationship that exhibits the problem, an invoice and its lineitems: the invoice has a "total" attribute that sums its lineitems' "amount" attributes.
class LineItem < ActiveRecord::Base
# t.integer :amount
# t.integer :invoice_id
belongs_to :invoice, :touch => true
class Invoice < ActiveRecord::Base
# t.integer :total
self.total = line_items.map(&:amount).sum
With these models, script/console:
=> #<Invoice id: 1, total: 0, created_at: "2009-09-30 05:04:41", updated_at: "2009-09-30 05:04:41">
>> LineItem.create(:invoice_id => 1, :amount => 10)
- The line-item saves itself, then the generated #belongs_to_touch_after_save_or_destroy_for_invoice causes the invoice's updated_at to be changed, then the invoice to be saved.
- Saving the invoice fires its before_save callback, which sums up its total, which is then saved.
- Because accepts_nested_models_for turns on autosave for the association, the invoice then saves its lineitems.
- Saving the lineitems triggers #belongs_to_touch_after_save_or_destroy_for_invoice again, and the cycle repeats until the stack overflows.
I was hoping there's a way to break this cycle. I don't want to get rid of my use of :touch; it not only simplifies doing this sort of summarization in the database, it also makes it easy to come up with cache keys for UI fragments (I can just use the parent model's updated_at time).
So, I focused on why the lineitems were being saved after the parent model was touched; this let me to AutosaveAssociation#save_collection_association
, which iterates over the loaded records and saves them, apparently even if they're not new or dirty (the #save call on line 295) - this surprised me, so I tried hacking my version to change line 294 from
"elsif autosave" to "elsif autosave && record.changed?" -- and my problem went away.
I'm starting down the path of writing tests and creating a patch; before I do, anyone see why that save (on line 295) needs to happen even if the record isn't dirty?