Shpigford
Shpigford

Reputation: 25368

ActiveRecord: How can I clone nested associations?

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.

Upvotes: 28

Views: 19992

Answers (5)

lucasarruda
lucasarruda

Reputation: 1482

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.

Upvotes: 3

Saurabh Goyal
Saurabh Goyal

Reputation: 51

Shouldn't it be..

  new_question.answers << new_answer
end

new_template.questions << new_question

Upvotes: 0

Rob
Rob

Reputation: 4434

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

Upvotes: -2

Vaughn Draughon
Vaughn Draughon

Reputation: 1388

You may also like the Amoeba gem for ActiveRecord 3.2.

In your case, you probably want to make use of the nullify, regex or prefix options available in the configuration DSL.

It supports easy and automatic recursive duplication of has_one, has_many and has_and_belongs_to_many associations, field preprocessing and a highly flexible and powerful configuration DSL that can be applied both to the model and on the fly.

be sure to check out the Amoeba Documentation but usage is pretty easy...

just

gem install amoeba

or add

gem 'amoeba'

to your Gemfile

then add the amoeba block to your model and run the dup method as usual

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

class PostsController < ActionController
  def some_method
    my_post = Post.find(params[:id])
    new_post = my_post.dup
    new_post.save
  end
end

You can also control which fields get copied in numerous ways, but for example, if you wanted to prevent comments from being duplicated but you wanted to maintain the same tags, you could do something like this:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    exclude_field :comments
  end
end

You can also preprocess fields to help indicate uniqueness with both prefixes and suffixes as well as regexes. In addition, there are also numerous options so you can write in the most readable style for your purpose:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    include_field :tags
    prepend :title => "Copy of "
    append :contents => " (copied version)"
    regex :contents => {:replace => /dog/, :with => "cat"}
  end
end

Recursive copying of associations is easy, just enable amoeba on child models as well

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
  has_many :ratings

  amoeba do
    enable
  end
end

class Rating < ActiveRecord::Base
  belongs_to :comment
end

The configuration DSL has yet more options, so be sure to check out the documentation.

Enjoy! :)

Upvotes: 3

fl00r
fl00r

Reputation: 83680

Use deep_clonable gem

new_survey = original_survey.clone :include => [:questions => :answers]

Upvotes: 46

Related Questions