:autosave ignored on has_many relation -- what am I missing? - ruby-on-rails

I have a pair of classes:
class Collection < ActiveRecord::Base
has_many :items, autosave: true
end
class Item < ActiveRecord::Base
belongs_to :collection
end
From the docs:
When :autosave is true all children is saved, no matter whether they are new records:
But when I update an Item and save its parent Collection, the Item's upated attributes don't get saved:
> c = Collection.first
=> #<Collection id: 1, name: "collection", created_at: "2012-07-23 00:00:10", updated_at: "2012-07-23 00:00:10">
> i = c.items.first
=> #<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">
> i.name = 'new name'
=> "new name"
> c.save
=> true
> Collection.first.items
=> [#<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">]
So, what am I missing?
I'm using Rails 3.2.5 and Ruby 1.9.2.
So I've done some digging about in the source of ActiveRecord. We can get hold of c's autosave assocations:
> c.class.reflect_on_all_autosave_associations
=> [#<ActiveRecord::Reflection::AssociationReflection:0x007fece57b3bd8 #macro=:has_many, #name=:items, #options={:autosave=>true, :extend=>[]}, #active_record=Collection(id: integer, name: string, created_at: datetime, updated_at: datetime), #plural_name="items", #collection=true, #class_name="Item", #klass=Item(id: integer, collection_id: integer, name: string, created_at: datetime, updated_at: datetime), #foreign_key="collection_id", #active_record_primary_key="id", #type=nil>]
I think this illustrates that the association has been set up for autosaving.
We can then get the instance of the association corresponding to c:
> a = c.send :association_instance_get, :items
=> #<ActiveRecord::Associations::HasManyAssociation:0x007fece738e920 #target=[#<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">], #reflection=#<ActiveRecord::Reflection::AssociationReflection:0x007fece57b3bd8 #macro=:has_many, #name=:items, #options={:autosave=>true, :extend=>[]}, #active_record=Collection(id: integer, name: string, created_at: datetime, updated_at: datetime), #plural_name="items", #collection=true, #class_name="Item", #klass=Item(id: integer, collection_id: integer, name: string, created_at: datetime, updated_at: datetime), #foreign_key="collection_id", #active_record_primary_key="id", #type=nil>, #owner=#<Collection id: 1, name: "collection", created_at: "2012-07-23 00:00:10", updated_at: "2012-07-23 00:00:10">, #updated=false, #loaded=true, #association_scope=[#<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">], #proxy=[#<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">], #stale_state=nil>
We can then find the actual objects that are associated via this association:
> a.target
=> [#<Item id: 1, collection_id: 1, name: "item1", created_at: "2012-07-23 00:00:25", updated_at: "2012-07-23 00:00:25">]
The object found here does not have update that I'd made earlier.

The problem here is the line
i = c.items.first
This line pulls the correct item from the database, but doesn't attach it to the collection c. It is a distinct ruby object from the object
i = c.items[0]
If you replace the first line with the second your example will work.

Related

Ruby on rails has many through on a concern using a polymorphic association

I have a problem with using has_many through on a concern and from a polymorphic association.
So I have 3 models and concern here:
https://gist.github.com/andreorvalho/5c2f0e3800fbb126df85
my problem is when I create a competition and do:
competition.content_permission_sources I get all the correct values, but when I do
competition. permission_sources I get an empty collection proxy, even though the content_permission_source has a permission_source:
2.1.1 :022 > c.content_permission_sources
=> #<ActiveRecord::Associations::CollectionProxy [#<ContentPermissionSource id: 1, permissible_id: 1, permissible_type: "Competition", permission_source_id: 5, is_forced: nil, created_at: "2014-05-26 19:08:40", updated_at: "2014-05-26 19:08:40">, #<ContentPermissionSource id: 2, permissible_id: 1, permissible_type: "Competition", permission_source_id: 4, is_forced: nil, created_at: "2014-05-26 19:08:40", updated_at: "2014-05-26 19:08:40">, #<ContentPermissionSource id: 3, permissible_id: 1, permissible_type: "Competition", permission_source_id: 6, is_forced: nil, created_at: "2014-05-26 19:08:40", updated_at: "2014-05-26 19:08:40">]>
2.1.1 :023 > c.permission_sources
=> #<ActiveRecord::Associations::CollectionProxy []>
2.1.1 :024 > c.content_permission_sources.first.permission_source
=> #<PermissionSource id: 5, collectable_id: 1, collectable_type: "Site", source_code: 690089, source_name: "fallback", permission_type: "newsletter", country_code: "dk", created_at: "2014-05-26 19:03:01", updated_at: "2014-05-26 19:03:01">
Anybody has an idea of what I am doing wrong and how can I make sure I can access it correctly?

Rails ActiveRecord : scope incoherence

In my model I have :
#models/friend.rb
scope :approved_friend, where(:approved => true)
And the Rails console outputs :
User.find(2).friends
=> [#<Friend id: 18, user_id: 2, approved: true, created_at: "2013-04-23 09:18:59", updated_at: "2013-04-23 09:18:59", friend_id: 1>]
User.find(2).friends.approved_friend
=> []
Notice that approved is true in the output ...
Where it gets crazy is here :
User.find(1).friends.approved_friend
=> [#<Friend id: 19, user_id: 1, approved: true, created_at: "2013-04-23 09:19:36", updated_at: "2013-04-23 09:19:36", friend_id: 2>]
Am-I missing something ?
EDIT :
On one hand you have this query :
SELECT "friends".* FROM "friends" WHERE "friends"."user_id" = 2
=> [#<Friend id: 18, user_id: 2, approved: true, created_at: "2013-04-23 09:18:59", updated_at: "2013-04-23 09:18:59", friend_id: 1>]
On the other hand, you've got this (query through scope) :
SELECT "friends".* FROM "friends" WHERE "friends"."user_id" = 2 AND "friends"."approved" = 't'
=> []
Since the :status field is in the Friend model, you might have to change the scope to this
scope :approved_friend, where('friends.approved' => true).includes(:friend)

Why is my after_save callback stopping my ActiveRecord association from saving properly?

When I comment out my after_save call back, my ActiveRecord associations work just fine. In Rails Console, you'd see:
> #report = Report.create :name => "foo"
=> #<Report id: 9, name: "foo", created_at: "2013-03-05 09:51:55", updated_at: "2013-03-05 09:51:55">
> #question = #report.questions.create :description => "bar"
=> #<Question id: 18, standard_id: nil, description: "bar", element_id: nil, condition_id: nil, blueprint_name: nil, blueprint_url: nil, created_at: "2013-03-05 09:52:32", updated_at: "2013-03-05 09:52:32", additive: false, instructions: nil>
> #report.questions
=> [#<Question id: 18, standard_id: nil, description: "bar", element_id: nil, condition_id: nil, blueprint_name: nil, blueprint_url: nil, created_at: "2013-03-05 09:52:32", updated_at: "2013-03-05 09:52:32", additive: false, instructions: nil>]
> #question.reports
=> [#<Report id: 9, name: "foo", created_at: "2013-03-05 09:51:55", updated_at: "2013-03-05 09:51:55">]
However, the associations stop working when I add the following after_save callback to question.rb:
def create_matching_surveys
self.reports.each do |report|
report.reviews.each do |review|
review.competitors.each do |competitor|
competitor.surveys.find_or_create_by_question_id(self.id)
end
end
end
end
Then, in Rails Console, you get:
> #report = Report.create :name => "foo"
=> #<Report id: 13, name: "foo", created_at: "2013-03-05 10:20:51", updated_at: "2013-03-05 10:20:51">
> #question = #report.questions.create :description => "bar"
=> #<Question id: 24, standard_id: nil, description: "bar", element_id: nil, condition_id: nil, blueprint_name: nil, blueprint_url: nil, created_at: "2013-03-05 10:21:02", updated_at: "2013-03-05 10:21:02", additive: false, instructions: nil>
> #report.questions
=> [#<Question id: 24, standard_id: nil, description: "bar", element_id: nil, condition_id: nil, blueprint_name: nil, blueprint_url: nil, created_at: "2013-03-05 10:21:02", updated_at: "2013-03-05 10:21:02", additive: false, instructions: nil>]
> #question.reports
=> []
This happens whether or not the report has reviews that have competitors.
The strange thing is I thought the callback was meant to happen after the question was saved? So by rights the association should save too before any of this happens, right?
How do I fix it?
UPDATE
I think I have to call the callback in the right spot in the object's life cycle, but I can't find that spot. Here's why I think this:
> #report = Report.create :name => "foo"
=> #<Report id: 20, name: "foo", created_at: "2013-03-05 12:29:35", updated_at: "2013-03-05 12:29:35">
> #question = #report.questions.create :description => "bar"
=> #<Question id: 31, standard_id: nil, description: "bar", element_id: nil, condition_id: nil, blueprint_name: nil, blueprint_url: nil, created_at: "2013-03-05 12:30:14", updated_at: "2013-03-05 12:30:14", additive: false, instructions: nil>
> #question.reports
=> []
> #question.update_attributes :description => "foo"
=> true
> #question.reports
=> [#<Report id: 20, name: "foo", created_at: "2013-03-05 12:29:35", updated_at: "2013-03-05 12:29:35">]
BTW, the method is now in question_observer.rb:
class QuestionObserver < ActiveRecord::Observer
def after_save(model)
model.reload
model.reports.reload
model.reports.each do |report|
report.reviews.each do |review|
review.competitors.each do |competitor|
competitor.surveys.find_or_create_by_question_id(model.id)
end
end
end
return true
end
end
The answer was to use a neat new callback hook called after_commit which was introduced with Rails 3.
See http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit.
The only issue is after_commit doesn't work "out of the box" with transactional fixtures, but there are plenty of solutions out there, and I found this one worked well for me: https://supportbee.com/devblog/2012/01/14/testing-after_commitafter_transaction-with-rspec/

.first returning wrong object type

If you look at the four method calls below, Service.first returns a Service object, Salon.first returns a Salon object, etc. But TransactionItem.first returns a Service object. Why could this be?
ruby-1.8.7-p334 :001 > Service.first
=> #<Service id: 147, name: "Fub", salon_id: 2, created_at: "2011-08-10 18:00:07", updated_at: "2011-08-10 18:00:12", price: nil, active: true, archived: true>
ruby-1.8.7-p334 :002 > Salon.first
=> #<Salon id: 1, name: "The Cheeky Strut", created_at: nil, updated_at: nil, address_id: nil, email: nil>
ruby-1.8.7-p334 :003 > Product.first
=> #<Product id: 1, name: "Herbal Essences Shampoo", retail_price: #<BigDecimal:10305f1f0,'0.1E2',9(18)>, wholesale_price: nil, sku: "", salon_id: 2, created_at: "2011-07-08 01:35:48", updated_at: "2011-07-08 01:35:48", archived: false>
ruby-1.8.7-p334 :004 > TransactionItem.first
=> #<Service id: 63, created_at: "2011-08-30 20:05:57", updated_at: "2011-08-30 20:05:57", price: #<BigDecimal:10303eba8,'0.18E2',9(18)>>
ruby-1.8.7-p334 :005 >
This is what my app/models/transaction_item.rb looks like:
class TransactionItem < ActiveRecord::Base
belongs_to :transaction
belongs_to :stylist
end
I blew away the TransactionItem table via a migration, then created a brand new migration to re-create it. That seems to have fixed the problem.

Update attribute in callback Rails 3

Following on from this question, I have spent the day trying to add a cumulative running sales total to my sales table. It's a bit tricky (for me) because I want a running total for sales where the isbn_id is the same, and, within that set, records where the channel_id is the same - ranked by invoice_date. This is all so I can calculate royalties on a particular range of units sold.
Here's my non-working callback code, in the Sale model:
before_save :runningtotal
private
def runningtotal
#sale = Sale.order("invoice_date ASC")
#lastbal = #sale.find_all_by_isbn_id(#isbn).group_by(&:channel)
#that sucessfully gets all sales ranked by date ascending, then groups them by channel, just for the current isbn.
#lastbal.each do |channel, sale|
sale.each_with_index do |sale, i|
previous_sale = sale[i-1] unless i==0
next unless previous_sale
#total_quantity = previous_sale.quantity + :quantity
write_attribute(:total_quantity,#total_quantity)
end
end
end
Is this roughly how a callback should be written - just in the model? Does it just run magically before_save of a new sale?
My core question is: how can I update the attribute "total_quantity" to be the sum of "quantity" for the current record, and "total_quantity" for the previous record by date, in a before_save callback, within the constraints of the finds for isbn_id and channel_id?
Here's the output of the find:
ruby-1.9.2-p180 :025 > #lastbal = #sale.find_all_by_isbn_id(#isbn).group_by(&:channel)
=> {#<Channel id: 4, isbn_id: nil, channel_name: "Gratis", created_at: "2011-05-26 11:08:22", updated_at: "2011-05-26 11:08:22">=>[#<Sale id: 26, isbn_id: 2, quantity: 10000, value: 40000, currency: "", total_quantity: nil, created_at: "2011-05-26 11:11:30", updated_at: "2011-05-26 11:11:30", customer: "6", retail_price: nil, discount: nil, channel_id: 4, invoice_date: "2011-05-18", rule_id: nil, trenche: nil>], #<Channel id: 1, isbn_id: nil, channel_name: "Home", created_at: "2011-05-16 19:47:27", updated_at: "2011-05-16 19:47:27">=>[#<Sale id: 22, isbn_id: 2, quantity: 1000, value: 5193, currency: "", total_quantity: nil, created_at: "2011-05-25 19:46:03", updated_at: "2011-05-25 19:46:03", customer: "a", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2011-05-11", rule_id: nil, trenche: nil>, #<Sale id: 24, isbn_id: 2, quantity: 1000, value: 4394, currency: "", total_quantity: nil, created_at: "2011-05-26 09:48:16", updated_at: "2011-05-26 09:48:16", customer: "g", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2011-05-10", rule_id: nil, trenche: nil>, #<Sale id: 25, isbn_id: 2, quantity: 1000, value: 4394, currency: "", total_quantity: nil, created_at: "2011-05-26 10:02:38", updated_at: "2011-05-26 10:02:38", customer: "g", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2011-05-05", rule_id: nil, trenche: nil>, #<Sale id: 21, isbn_id: 2, quantity: 1000, value: 5193, currency: "", total_quantity: nil, created_at: "2011-05-25 14:12:45", updated_at: "2011-05-25 14:12:45", customer: "a", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2010-10-15", rule_id: nil, trenche: nil>, #<Sale id: 13, isbn_id: 2, quantity: 50, value: 159, currency: "", total_quantity: nil, created_at: "2011-05-25 12:33:09", updated_at: "2011-05-25 12:33:09", customer: "a", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2010-01-01", rule_id: nil, trenche: nil>, #<Sale id: 14, isbn_id: 2, quantity: 25, value: 129, currency: "", total_quantity: nil, created_at: "2011-05-25 12:33:23", updated_at: "2011-05-25 12:33:23", customer: "", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2010-01-01", rule_id: nil, trenche: nil>, #<Sale id: 12, isbn_id: 2, quantity: 100, value: 415, currency: "", total_quantity: nil, created_at: "2011-05-25 12:32:50", updated_at: "2011-05-25 15:13:21", customer: "a", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2001-10-01", rule_id: nil, trenche: nil>, #<Sale id: 11, isbn_id: 2, quantity: 500, value: 2197, currency: "", total_quantity: nil, created_at: "2011-05-25 12:32:24", updated_at: "2011-05-25 15:11:20", customer: "a", retail_price: nil, discount: nil, channel_id: 1, invoice_date: "2000-10-01", rule_id: nil, trenche: nil>], #<Channel id: 2, isbn_id: nil, channel_name: "Export", created_at: "2011-05-16 19:47:35", updated_at: "2011-05-16 19:47:35">=>[#<Sale id: 23, isbn_id: 2, quantity: 2000, value: 5000, currency: "", total_quantity: nil, created_at: "2011-05-26 09:16:15", updated_at: "2011-05-26 09:16:15", customer: "v", retail_price: nil, discount: nil, channel_id: 2, invoice_date: "2011-05-02", rule_id: nil, trenche: nil>, #<Sale id: 17, isbn_id: 2, quantity: 242, value: 657, currency: "", total_quantity: nil, created_at: "2011-05-25 12:34:24", updated_at: "2011-05-25 12:34:24", customer: "b ", retail_price: nil, discount: nil, channel_id: 2, invoice_date: "2010-10-15", rule_id: nil, trenche: nil>, #<Sale id: 18, isbn_id: 2, quantity: 54, value: 194, currency: "", total_quantity: nil, created_at: "2011-05-25 12:34:44", updated_at: "2011-05-25 12:34:44", customer: "b ", retail_price: nil, discount: nil, channel_id: 2, invoice_date: "2010-10-15", rule_id: nil, trenche: nil>, #<Sale id: 15, isbn_id: 2, quantity: 135, value: 377, currency: "", total_quantity: nil, created_at: "2011-05-25 12:33:48", updated_at: "2011-05-25 12:33:48", customer: "b ", retail_price: nil, discount: nil, channel_id: 2, invoice_date: "2010-09-15", rule_id: nil, trenche: nil>, #<Sale id: 16, isbn_id: 2, quantity: 433, value: 830, currency: "", total_quantity: nil, created_at: "2011-05-25 12:34:06", updated_at: "2011-05-25 12:34:06", customer: "b ", retail_price: nil, discount: nil, channel_id: 2, invoice_date: "2010-09-15", rule_id: nil, trenche: nil>]}
Here's the columns in my Sale model:
# id :integer not null, primary key
# isbn_id :integer
# quantity :integer
# value :integer
# currency :string(255)
# total_quantity :integer
# created_at :datetime
# updated_at :datetime
# customer :string(255)
# retail_price :integer
# discount :decimal(, )
# channel_id :integer
# invoice_date :date
# rule_id :integer
Thanks so much in advance.
UPDATE: final solution.
Really not sure that this counts as 'giving back to the community' as it's comically verbose, not DRY, full of puts which I used to figure out all the bugs, and badly formatted to boot, but heck, I'm a noob at and the very least I can come back here and laugh at myself in a few years when I know what I'm doing. So, here's my final solution, in Sale.rb. Poor overstuffed model. I will refactor this, one day.
before_save :runningtotal
after_commit :refresh
private
def runningtotal
# get the latest sale that matches the new sale's isbn and channel id, then rank by invoice date descending, and get the first record:
lastsale = Sale.where(:isbn_id => self.isbn_id).where(:channel_id => self.channel_id).order("invoice_date DESC").first
allsales = Sale.where(:isbn_id => self.isbn_id).where(:channel_id => self.channel_id).order("invoice_date DESC")
# set the total_quantity field in the new sales record to its quantity + the last sale's total.
if allsales.maximum(:invoice_date).nil?
puts "runningtotal thinks the max of invoice date in the allsales relation is nil"
puts "and runningtotal is setting total_quantity on the new sale to be #{self.quantity + (lastsale.try(:total_quantity) || 0)}"
self.total_quantity = self.quantity + (lastsale.try(:total_quantity) || 0)
else
if self.invoice_date < allsales.maximum(:invoice_date)
puts "the runningtotal method has been skipped because runningtotal thinks the current invoice date is less than the highest invoice date in the allsales relation"
else
puts "this is a normal entry so runningtotal has set the total quantity to be #{self.quantity + (lastsale.try(:total_quantity) || 0) }"
self.total_quantity = self.quantity + (lastsale.try(:total_quantity) || 0)
end
end
end
def refresh
allsales = Sale.where(:isbn_id => self.isbn_id).where(:channel_id => self.channel_id).order("invoice_date ASC")
#if the runningtotal callback hasn't run, the total quantity will be nil, and nil triggers this after_commit callback
if total_quantity.nil?
puts "running refresh callback"
puts "here's a sample parameter pass: id: #{id} quantity: #{quantity} date: #{invoice_date} "
puts "allsales class is #{allsales.class}"
# if the new sale that's being saved has a date that's before any previous sale...
puts "before the if, refresh thinks that the earliest invoice date is #{allsales.minimum(:invoice_date)} and that invoice date is #{invoice_date}"
if invoice_date <= allsales.minimum(:invoice_date)
puts "date earlier than existing sales dates"
puts "refresh thinks that the earliest invoice date is #{allsales.minimum(:invoice_date)} and that invoice date is #{invoice_date}"
#... then set its total_quantity to the sale quantity...
update_attribute(:total_quantity, quantity)
puts "total_qty updated with qty"
# ... and update all subsequent records' total_quantity (skipping the before_save callback which would trigger an infinite loop).
allsales.each_with_index do |sale, i|
previous_sale = allsales[i-1] unless i==0
next unless previous_sale
puts "getting qty out of arel when date earlier than others: #{previous_sale.quantity}"
puts "this is adding #{quantity} to #{previous_sale.quantity } which is #{quantity + previous_sale.total_quantity }"
Sale.skip_callback(:save, :before, :runningtotal )
sale.update_attribute(:total_quantity, (sale.quantity + previous_sale.total_quantity ))
Sale.set_callback(:save, :before, :runningtotal)
end
else
# if the invoice date is within the min and max range of the previous sales...
# ... update all previous and subsequent records' total_quantity (skipping the before_save callback which would trigger an infinite loop).
allsales.each_with_index do |sale, i|
previous_sale = allsales[i-1] unless i==0
next unless previous_sale
puts "getting qty out of arel within existing date range: #{previous_sale.quantity}"
puts "this is adding #{quantity} to #{previous_sale.quantity } which is #{quantity + previous_sale.total_quantity }"
Sale.skip_callback(:save, :before, :runningtotal )
sale.update_attribute(:total_quantity, (sale.quantity + previous_sale.total_quantity ))
Sale.set_callback(:save, :before, :runningtotal )
end
end
end
end
Yes, using before_save in the model will run that every time it is saved, whether new or updated. Thus you need to watch out in calculations the expect the current (new) record to not exist yet. ;) You might want to use before_save, :on => :create to limit it to the creation action.
However, if I understand your english statement of the problem, your code is rather convoluted. I don't even see where #isbn is set, that could be dangerous...
Does this need to update the total on other objects this isbn and channel? Usually it's better form to simply calculate that as needed rather than trying to cache the total in every record.
within the callback, self is the current (new?) record, so use it to refer to new values.
#sale = Sale.order("invoice_date ASC")
#lastbal = #sale.find_all_by_isbn_id(#isbn).group_by(&:channel)
can be replaced by this, I think:
#lastbal = Sale.order("invoice_date ASC").where(:isbn_id => self.isbn_id).group_by(&:channel)
I'm assuming that #isbn is actually the new record's isbn.
From there, I'm not sure if you are only intending to update the new record or the old ones... If you want to update the current record, just set the attribute and exit the callback, and it will be saved when the rest is saved:
self.total_quantity = previous_sale.quantity + self.quantity
If you are intending to update the other objects too, then we have to update those objects and save them. I don't see that happening at all here in your code.
Your code goes through several loops, possibly hitting the write_attribute several times... that doesn't make sense.
If you mean you want to find the last record that matches the current isbn and channel to update the new record, here's what I would do:
def runningtotal
lastsale = Sale.where(:isbn_id => self.isbn_id).
where(:channel_id => self.channel_id).
order("invoice_date DESC").first
# that should be the latest sale that matches
# the current isbn and channel
self.total_quantity = self.quantity + (lastsale.try(:total_quantity) || 0)
# watch out for nil if no previous record exists ^
end
`

Resources