1

Update 2

Incidentally, as I was typing Update 1 @muffinista posted the same conclusion I came to in Update 1 and I just wanted to add clarification that I am not sniping his answer and updating the question randomly. After thinking it through some more after having posted the question, I realized what the issue was. While @muffinista posted the cause, his solution was inadequate - that's why I am leaving Update 1 intact - because I found a better solution and it makes sense for others to get the full context.

Update 1

So I figured what is causing this error, an infinite loop.

I am trying to save the client record within an after_save callback on the Client record. So it keeps trying to save the client record and executing the after_save callback which tries to save the client_record.

How can I achieve what I want, i.e. updating this client.weighted_score attribute whenever the record is updated without jumping into this loop?

Original Question

I have this callback:

after_save :calculate_weighted_score, :if => Proc.new { |c| c.score.present? }

def calculate_weighted_score
    # Sum products of weight & scores of each attribute
        client = self
        weight = Weight.first
        score = self.score
        client.weighted_score = (weight.firm_size * score.firm_size) + (weight.priority_level * score.priority_level) + 
                (weight.inflection_point * score.inflection_point) + (weight.personal_priority * score.personal_priority) +
                (weight.sales_priority * score.sales_priority) + (weight.sales_team_priority * score.sales_team_priority) +
                (weight.days_since_contact * score.days_since_contact) + (weight.does_client_vote * score.does_client_vote) +
                (weight.did_client_vote_for_us * score.did_client_vote_for_us) + (weight.days_until_next_vote * score.days_until_next_vote) +
                (weight.does_client_vote_ii * score.does_client_vote_ii) + (weight.did_client_vote_ii_for_us * score.did_client_vote_ii_for_us) + 
                (weight.days_until_vote_ii * score.days_until_vote_ii)
        client.o
            # self.save
        # client.update_attributes(:weighted_score => weighted_score)
end

This is an example of the state of a Client record before this callback is run:

#<Client:0x007fe00dbcea90> {
                   :id => 10,
                 :name => "Manta-Jar Gale",
                :email => "mj@gmail.com",
                :phone => 8769876435,
              :firm_id => 1,
           :created_at => Fri, 23 Nov 2012 23:50:09 UTC +00:00,
           :updated_at => Tue, 27 Nov 2012 17:50:01 UTC +00:00,
              :user_id => 1,
    :personal_priority => true,
         :last_contact => Sat, 08 Jan 2011,
                 :vote => true,
        :vote_for_user => false,
            :next_vote => Thu, 02 Jan 2014,
              :vote_ii => true,
       :vote_ii_for_us => true,
         :next_vote_ii => Mon, 01 Jul 2013,
       :weighted_score => nil,
            :firm_size => 100.0
}

Notice the weighted_score => nil attribute.

After the callback, this same record looks like this:

#<Client:0x007fe00dbcea90> {
                   :id => 10,
                 :name => "Manta-Jar Gale",
                :email => "mj@gmail.com",
                :phone => 8769876435,
              :firm_id => 1,
           :created_at => Fri, 23 Nov 2012 23:50:09 UTC +00:00,
           :updated_at => Tue, 27 Nov 2012 17:50:01 UTC +00:00,
              :user_id => 1,
    :personal_priority => true,
         :last_contact => Sat, 08 Jan 2011,
                 :vote => true,
        :vote_for_user => false,
            :next_vote => Thu, 02 Jan 2014,
              :vote_ii => true,
       :vote_ii_for_us => true,
         :next_vote_ii => Mon, 01 Jul 2013,
       :weighted_score => 9808,
            :firm_size => 100.0
}

Notice the weighted_score => 9808 attribute.

So I know that the callback calculate_weighted_score is being run, and the entire callback seems to be correct up until the assignment of the client.weighted_score. The issue is, the log shows no UPDATE db transaction for that attribute:

Started PUT "/clients/10" for 127.0.0.1 at 2012-11-27 12:50:01 -0500
Processing by ClientsController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"J172LuZQ=", "client"=>{"name"=>"Manta-Jar Gale", "email"=>"mj@gmail.com", "phone"=>"8769876435", "firm_id"=>"1", "topic_ids"=>["1", "2", "9"], "personal_priority"=>"1", "last_contact"=>"2011-01-08", "vote"=>"1", "vote_for_user"=>"0", "next_vote"=>"2014-01-02", "vote_ii"=>"1", "vote_ii_for_us"=>"1"}, "commit"=>"Update Client", "id"=>"10"}
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
  Client Load (0.2ms)  SELECT "clients".* FROM "clients" WHERE "clients"."user_id" = 1 AND "clients"."id" = ? LIMIT 1  [["id", "10"]]
  Topic Load (0.3ms)  SELECT "topics".* FROM "topics" 
   (0.1ms)  begin transaction
  Topic Load (0.2ms)  SELECT "topics".* FROM "topics" WHERE "topics"."id" IN (1, 2, 9)
  Topic Load (0.1ms)  SELECT "topics".* FROM "topics" INNER JOIN "clients_topics" ON "topics"."id" = "clients_topics"."topic_id" WHERE "clients_topics"."client_id" = 10
   (0.5ms)  UPDATE "clients" SET "name" = 'Manta-Jar Gale', "updated_at" = '2012-11-27 17:50:01.856893' WHERE "clients"."id" = 10
  Firm Load (0.2ms)  SELECT "firms".* FROM "firms" WHERE "firms"."id" = 1 LIMIT 1
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
  Score Load (0.2ms)  SELECT "scores".* FROM "scores" WHERE "scores"."client_id" = 10 LIMIT 1
  SalesTeam Load (0.1ms)  SELECT "sales_teams".* FROM "sales_teams" WHERE "sales_teams"."id" = 2 LIMIT 1
  PriorityLevel Load (0.1ms)  SELECT "priority_levels".* FROM "priority_levels" WHERE "priority_levels"."id" = 1 LIMIT 1
  CACHE (0.0ms)  SELECT "scores".* FROM "scores" WHERE "scores"."client_id" = 10 LIMIT 1
  Max Load (0.2ms)  SELECT "maxes".* FROM "maxes" WHERE "maxes"."user_id" = 1 LIMIT 1
  CACHE (0.0ms)  SELECT "scores".* FROM "scores" WHERE "scores"."client_id" = 10 LIMIT 1
  Weight Load (0.2ms)  SELECT "weights".* FROM "weights" LIMIT 1
   (3.2ms)  commit transaction
Redirected to http://localhost:3000/clients
Completed 302 Found in 796ms (ActiveRecord: 26.4ms)

The only UPDATE transaction is for the edit to the name I made to test the callback.

I know there is no UPDATE transaction, because technically I am not saving the record.

But when I try to do any of the commented out statements - i.e. client.save or client.update_attributes(....) I get a Stack Level Too Deep Error.

What is causing this and how can I save this record?

marcamillion
  • 32,933
  • 55
  • 189
  • 380
  • Similar question answered before http://stackoverflow.com/questions/215885/using-the-after-save-callback-to-modify-the-same-object-without-triggering-the-c . – dimuch Nov 27 '12 at 18:12
  • @marcamillion Please don't change your question after others have answered it. Accept an answer, then post a new question if you need to. – benzado Nov 27 '12 at 18:33
  • Heh...perhaps if you had looked at the timestamp, you would notice that the time that I edited and the time that @muffinista posted his response were pretty close, so how about giving me the benefit of the doubt and assuming that I figured it out around the same time he was posting his answer? Either way, his answer only half-complete. I have added a more complete answer. – marcamillion Nov 27 '12 at 18:35

3 Answers3

3

As indicated by @muffinista (and specified in the update to the question itself), the issue here is that the callback is trying to save the record which is calling the callback which is trying to save the record...creating a Stack Overflow (stole that joke from @ivan on this similar question).

It seems the best answer is to use update_column - the docs can be seen here.

That does the same thing that update_attributes does, but doesn't do validation or callbacks - which is fine for what I want in this particular instance.

Of special note, the format of update_column is not the same as update_attributes.

What you do is:

client.update_column(:weighted_score, weighted_score)

Where :weighted_score is my column name, and weighted_score is the local variable I set in the code example above that did my calculation for me.

For what it's worth, I got this answer from this SO answer.

Community
  • 1
  • 1
marcamillion
  • 32,933
  • 55
  • 189
  • 380
1

Your callback is running after the original record is saved, but you're modifying the record and trying to re-save it, which triggers the callback again ad infinitum. This is basically a duplicate question, it's covered here for example: when to update record after save?

Community
  • 1
  • 1
muffinista
  • 6,676
  • 2
  • 30
  • 23
  • You figured out the problem for sure. The issue is the solution. The link you posted does not solve the issue for Rails 3. Pushing the save within the callback to a background queue gives you the same thing - an infinite loop of background tasks. Also the `update_without_callback` method seems to be deprecated in Rails 3. Any other solutions that will work for Rails 3? – marcamillion Nov 27 '12 at 18:14
  • 1
    I try to avoid this situation in the first place if possible, but obviously that can't always be done. In lieu of that, using update_column as you've discovered already is probably the best option. – muffinista Nov 27 '12 at 18:36
  • I agree. I try to avoid it too - but in this particular instance it is unavoidable. – marcamillion Nov 27 '12 at 18:38
1

How about using a before_save filter instead?

John Naegle
  • 8,077
  • 3
  • 38
  • 47
  • It messes up the flow. I figured it out though, I am gonna post an answer. – marcamillion Nov 27 '12 at 18:27
  • I see nothing in your question that explains why a `before_save` would not do? This would be the cleanest solution. – nathanvda Nov 27 '12 at 18:33
  • You are right, I didn't post the rest of the model - because I felt like it would add too much overhead to an already pretty involved post. I have about 4 - 5 other callbacks, some of which are `before_save`s. So, for all intents and purposes, you just have to take my word that I tried doing a `before_save` and it messed up the entire flow altogether, because of the other `callbacks` involved. – marcamillion Nov 27 '12 at 18:37