在Rails中克隆记录,是否可以克隆关联和进行深度复制?

36

我正在Rails中使用.clone复制一条记录...

  new_blerg = Blerg.find(1).clone

这条记录有大量的关联,并且这些关联还有它们自己的关联。

有没有一种方法可以深度复制记录并克隆它,使它被克隆时所有这些关联也被克隆?


这个回答解决了你的问题吗?Ruby on Rails 对象及其属性的深拷贝/深克隆 - undefined
3个回答

33
你可能会从ActiveRecord 3.2中受益,使用Amoeba gem。它支持has_onehas_manyhas_and_belongs_to_many关联的易于递归复制、字段预处理以及高度灵活和强大的配置DSL(可应用于模型和即时应用)。请务必查看Amoeba文档,但使用方法非常简单...只需
gem install amoeba

或者添加

gem 'amoeba'

将其添加到Gemfile中

然后在你的模型中添加amoeba块,并像平常一样运行dup方法。

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

你的新帖子应该具有最初与其关联的所有标签,所有评论也应当被复制。您可以通过DSL禁用各种记录的复制,有关此内容的详细信息请参阅文档,但例如,如果您想保留标签,但不想保留评论,则可以执行以下操作:

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

  amoeba do
    include_field :comments
  end
end

或者使用独家语法

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

  amoeba do
    exclude_field :comments
  end
end

或者通过指定要识别的字段类型(并因此复制)

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

  amoeba do
    recognize :has_and_belongs_to_many
  end
end

这些不同的选项中,每个选项都应该会将新帖子重新关联到与旧帖子相同的标签,但不会复制评论。

如果启用了子记录,Amoeba还将自动递归进入子记录。

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

您还可以在字段前缀上添加一些额外的数据以指示唯一性

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
    prepend :title => "Copy of "
  end
end

除了可以使用prepend,你还可以将内容附加到给定字段的末尾,或者对其运行正则表达式。

祝使用愉快!:)


2
哇,这个宝石真的很棒。我不得不自己编写复制系统,但它并没有起作用,而你的宝石却非常好用。 - Amala
3
在这个例子中,应该将 ".dup" 改为 "new_post = my_post.amoeba_dup",以满足文档中的定义吗? - kibaekr
6
根据我找到的 README 历史记录,“截至2012年12月11日,Amoeba不再覆盖内置的ActiveRecord :: Base#dup方法,而是实现了自己的方法称为amoeba_dup...” - Sam

23

您需要编写自己的clone_with_associations方法,该方法通过指定的一组关联进行遍历。理论上,您可以编写一个使用reflect_on_all_associations的通用方法,但您需要在关联对象上执行相同的操作,这最终会创建一个生成无限记录的循环。

因此,只需编写自己的方法。类似于以下内容:

  #in Blerg
  has_many :foos
  has_many :bars #bars also have many chickens which we want to copy over as well
  def clone_with_associations
    new_blerg = self.dup
    new_blerg.save
    #simple association
    new_blerg.foos = self.foos
    #two-level association 
    self.bars.each do |bar|
      new_bar = bar.clone
      new_bar.save
      new_bar.chickens = bar.chickens 
      new_blerg.bars << bar
    end
    new_blerg
  end

现在你可以这样做

@new_blerg = Blerg.find(1).clone_with_associations

25
当执行new_blerg.foos = self.foos时,你将得到一个损坏的原始对象,因为它窃取了你的关联。你需要同时克隆它们。 - RocketR
1
这是最好的答案。在我看来,在这种情况下自己编写代码比使用 gem 更加简洁、容易和可控。 - hellion
RocketR - 很好的观点。我认为我假设 .foos 关系是“有并属于许多”,在这种情况下它是可以的,但如果 foo 属于 blerg,那么是的,它会改变相关的 foos。 - Max Williams
卡在@RocketR提到的第一条评论相同的点上。如果我需要更新has_many关联中的列,应该怎么办?def duplicate_records new_course = self.deep_clone new_course.image = File.open(self.image.file.file) if self.image.present? new_course.save new_course.chapters = self.chapters new_course.chapters.each do |chapter| new_chapter = chapter.clone new_chapter.image = File.open(chapter.image.file.file) if chapter.image.present? new_chapter.save end new_course end - LearningROR
@LearningROR 我认为你需要将 new_course.chapters.each do |chapter| 替换为 self.chapters.each do |chapter|。请记住,self 是被克隆的对象。 - Max Williams
显示剩余3条评论

20
同样,这个宝石似乎也很好用:https://github.com/moiristo/deep_cloneable,而且非常易于使用。

只需执行:

gem ‘deep_cloneable’, ‘~> 1.4.0’

然后:

pirate.deep_clone :include => :mateys


3
相较于“变形虫”,我发现这个更易实现 - 模型中不需要声明。 - Benjineer
1
是的,这比ameoba少得多,没有需要学习的声明或DSL。我认为它更像“railsy”。灵活性也很好。Rails应该将其作为AR方法添加。 - bwest87
1
新的喜爱宝石。 - Paul Danelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接