ActiveRecord: How can I clone nested associations?

Viewed 18791

I'm currently cloning a single-level association like this:

class Survey < ActiveRecord::Base
  def duplicate
    new_template = self.clone
    new_template.questions << self.questions.collect { |question| question.clone } 
    new_template.save   
  end
end

So that clones the Survey then clones the Questions associated with that survey. Fine. That works quite well.

But what I'm having trouble with is that each question has_many Answers. So Survey has_many Questions which has_many Answers.

I can't figure out how to clone the answers properly. I've tried this:

def duplicate
  new_template = self.clone

  self.questions.each do |question|
    new_question = question.clone
    new_question.save

    question.answers.each do |answer|
      new_answer = answer.clone
      new_answer.save
      new_question.answers << answer
    end

    new_template.questions << question
  end

  new_template.save   
end

But that does some weird stuff with actually replacing the original answers then creating new ones, so ID's stop matching correctly.

5 Answers

Without using gems, you can do the following:

class Survey < ApplicationRecord
  has_and_belongs_to_many :questions

  def copy_from(last_survey)
    last_survery.questions.each do |question|
      new_question = question.dup
      new_question.save

      questions << new_question
    end

    save
  end
  …
end

Then you can call:

new_survey = Survey.create
new_survey.copy_from(past_survey)

That will duplicate all questions from last Survey to new Survey and tie them.

You can also alias the rails dup method, as follows:

class Survey
   has_many :questions, :inverse_of=>:survey, :autosave=>true
   alias orig_dup dup
   def dup
       copy=orig_dup
       copy.questions=questions
       copy
   end
end

class Questions
   belongs_to :survey, :inverse_of=>:questions
   has_many :answers, :inverse_of=>:question, :autosave=>true
   alias orig_dup dup
   def dup
       copy=orig_dup
       copy.answers=answers
       copy
   end
end

class Answer
    belongs_to :question
end

and then you can do this

aaa = Survey.find(123).dup
aaa.save
Related