Zoinks10
Zoinks10

Reputation: 629

Why is my Rails method always being called on save (without an after_save callback set)?

In my rails model I have a series of hard-coded questions (loaded into the DB using YAML). In order to display these to the user I have a SaleQualifier model - this has_many questions and has_one answer.

In the SalesOpportunity show action (where the SaleQualifier will be displayed from). I create a new SaleQualifier and build the associated question:

def show
 @sales_opportunity = SalesOpportunity.includes(:company, :user, :timeline_events, :sale_contacts, :swots, :sale_competitors).find(params[:id])
 @sale_qualifier = SaleQualifier.new(sales_opportunity_id: params[@sales_opportunity.id])
 @answer = @sale_qualifier.answers.build
#if the question_id is blank it is the first question in the list
 if @sale_qualifier.question_id.blank?
  @question = Question.find_by_id(@sale_qualifier.next_question_id)
 else
  @question = Question.find_by_id(@sale_qualifier.question_id)
 end
end

The Answer model is set up to ensure that once it becomes saved and permanently tied to this SaleQualifier it will fire an after_save callback to update the SaleQualifier (adding a has_answer tag, finding the appropriate next_question_id etc).

class Answer < ActiveRecord::Base
 validates :answer_text, presence: true
 belongs_to :sale_qualifier
 after_save :update_sale_qualifier

 def update_sale_qualifier
  sale_qualifier.update_next_question
  sale_qualifier.save
 end
end

The sale_qualifier.save line is what is causing me the problem. Rather than purely saving the record, it saves the record AND calls the update_next_question action again. I don't know why, as there's no after_save callback in my SaleQualifier model:

class SaleQualifier < ActiveRecord::Base
 has_one :answer, :inverse_of => :sale_qualifier, dependent: :destroy
 accepts_nested_attributes_for :answer
 belongs_to :sales_opportunity
 validates :sales_opportunity_id, presence: true

def update_next_question
   #find the question that this SaleQualifier is associated with
   question = Question.find_by_id(self.question_id)
   #get its answer_type as this defines whether we need to look at the next_question_id_no or not
   ans_type = question.answer_type
   if ans_type == 'Text Field' || ans_type == 'Datetime' || ans_type == 'Integer' || ans_type == 'Boolean' && self.answer.answer_text == 'True'
    self.next_question_id = question.next_question_id_yes
    #if the answer_type is a boolean and the answer is no/false, then use the next_question_id_no to route the question
elsif ans_type == 'Boolean' && self.answer.answer_text == 'False'
    self.next_question_id = question.next_question_id_no
   end
   #mark the question as answered, in case we need to iterate over the same quesiton in future
   self.has_answer = true
   #create a new SaleQualifier with the question set as the next question, unless it already exists and hasn't been answered yet or unless the next question id leads us to a result (i.e. no questions left in the tree)
   SaleQualifier.create(question_id: self.next_question_id, sales_opportunity_id: self.sales_opportunity_id) unless Question.exists?(:id => self.next_question_id) || SaleQualifier.find_by(question_id: self.next_question_id) && SaleQualifier.find_by(question_id: self.next_question_id).has_answer == false
 end
end

Can anyone tell me why the update_next_question method is calling itself in a loop every time I save my SaleQualifier?

Upvotes: 0

Views: 127

Answers (1)

Taryn East
Taryn East

Reputation: 27747

So we figured out that there's a one-time feedback loop involved by the fact that Answer calls save on a SaleQualifer and SaleQualifier contains accepts_nested_attributes_for :answer.... so if you have a SaleQualifer that contains attributes for an Answer then you save the answer, it will use the after_save callback, then call save, which saves the SaleQualifer, which calls save on the answer... which calls the after_save callback again.

So yeah, changing that to an after_create hook fixes it by breaking the cycle on the sale_qualifier.save line.

Upvotes: 1

Related Questions