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 end class Invoice < ActiveRecord::Base # t.integer :total # t.timestamps has_many :line_items accepts_nested_attributes_for :line_items before_save :summarize def summarize self.total = line_items.map(&:amount).sum end end With these models, script/console:>> Invoice.create=> #<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)This fails: - 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<http://github.com/rails/rails/blob/v2.3.4/activerecord/lib/active_record/autosave_association.rb#L272>, 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? Thanks, ...Bryan --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Lawrence Pit
2009-Sep-30 21:47 UTC
Re: accepts_nested_models_for (& autosave) versus :touch
Hi Bryan, Pls create a ticket at http://rails.lighthouseapp.com, so we can move the discussion there. What you suggest makes sense. However, instead of modifying that line you''re referring to I suggest to modify +associated_records_to_validate_or_save+ instead. I think it should be: def associated_records_to_validate_or_save(association, new_record, autosave) if new_record association elsif autosave association.select { |record| record.new_record? || record.changed? } else association.select { |record| record.new_record? } end end Greets, Lawrence> 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 > end > > class Invoice < ActiveRecord::Base > # t.integer :total > # t.timestamps > has_many :line_items > accepts_nested_attributes_for :line_items > before_save :summarize > > def summarize > self.total = line_items.map(&:amount).sum > end > end > > With these models, script/console: > >> Invoice.create > => #<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) > > This fails: > - 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 > <http://github.com/rails/rails/blob/v2.3.4/activerecord/lib/active_record/autosave_association.rb#L272>, > 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? > > Thanks, > ...Bryan > > > >--~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Bryan Stearns
2009-Oct-01 02:01 UTC
Re: accepts_nested_models_for (& autosave) versus :touch
Thanks, Lawrence - I found the existing ticket 2578 which seems to be another case of this problem, and added my explanation and both our patches to it. (In the meantime, I found that both our patches cause lots of other tests to fail... I''m still trying to figure out why.) https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2578 Best, ...Bryan On Wed, Sep 30, 2009 at 2:47 PM, Lawrence Pit <lawrence.pit@gmail.com>wrote:> Hi Bryan, > > Pls create a ticket at http://rails.lighthouseapp.com, so we can move the > discussion there. > > What you suggest makes sense. However, instead of modifying that line > you''re referring to I suggest to modify > +associated_records_to_validate_or_save+ instead. I think it should be: > > def associated_records_to_validate_or_save(association, new_record, > autosave) > if new_record > association > elsif autosave > association.select { |record| record.new_record? || record.changed? > } > else > association.select { |record| record.new_record? } > end > end > > > Greets, > Lawrence > > 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 > end > > class Invoice < ActiveRecord::Base > # t.integer :total > # t.timestamps > has_many :line_items > accepts_nested_attributes_for :line_items > before_save :summarize > > def summarize > self.total = line_items.map(&:amount).sum > end > end > > With these models, script/console: > >> Invoice.create > => #<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) > > This fails: > - 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<http://github.com/rails/rails/blob/v2.3.4/activerecord/lib/active_record/autosave_association.rb#L272>, > 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? > > Thanks, > ...Bryan > > > > > > > >--~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group. To post to this group, send email to rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---