Module: ActiveRecord::Base::DeepCloneable

Included in:
ActiveRecord::Base
Defined in:
lib/deep_cloneable.rb

Defined Under Namespace

Classes: AssociationNotFoundException

Instance Method Summary collapse

Instance Method Details

#dup(*args, &block) ⇒ Object

clones an ActiveRecord model. if passed the :include option, it will deep clone the given associations if passed the :except option, it won’t clone the given attributes

Usage:

Cloning one single association

pirate.clone :include => :mateys

Cloning multiple associations

pirate.clone :include => [:mateys, :treasures]

Cloning really deep

pirate.clone :include => {:treasures => :gold_pieces}

Cloning really deep with multiple associations

pirate.clone :include => [:mateys, {:treasures => :gold_pieces}]

Cloning really deep with multiple associations and a dictionary

A dictionary ensures that models are not cloned multiple times when it is associated to nested models. When using a dictionary, ensure recurring associations are cloned first:

pirate.clone :include => [:mateys, {:treasures => [:matey, :gold_pieces], :use_dictionary => true }]

If this is not an option for you, it is also possible to populate the dictionary manually in advance:

dict = { :mateys => {} }
pirate.mateys.each{|m| dict[:mateys][m] = m.clone }
pirate.clone :include => [:mateys, {:treasures => [:matey, :gold_pieces], :dictionary => dict }]

Cloning a model without an attribute

pirate.clone :except => :name

Cloning a model without multiple attributes

pirate.clone :except => [:name, :nick_name]

Cloning a model without an attribute or nested multiple attributes

pirate.clone :include => :parrot, :except => [:name, { :parrot => [:name] }]


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/deep_cloneable.rb', line 60

def dup *args, &block
  options = args[0] || {}

  dict = options[:dictionary]
  dict ||= {} if options.delete(:use_dictionary)

  kopy = unless dict
    super()
  else
    tableized_class = self.class.name.tableize.to_sym
    dict[tableized_class] ||= {}
    dict[tableized_class][self] ||= super()
  end

  block.call(self, kopy) if block

  deep_exceptions = {}
  if options[:except]
    exceptions = options[:except].nil? ? [] : [options[:except]].flatten
    exceptions.each do |attribute|
      kopy.send(:write_attribute, attribute, self.class.column_defaults.dup[attribute.to_s]) unless attribute.kind_of?(Hash)
    end
    deep_exceptions = exceptions.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
  end

  if options[:include]
    Array(options[:include]).each do |association, deep_associations|
      if (association.kind_of? Hash)
        deep_associations = association[association.keys.first]
        association = association.keys.first
      end

      dup_options = deep_associations.blank? ? {} : {:include => deep_associations}
      dup_options.merge!(:except => deep_exceptions[association]) if deep_exceptions[association]
      dup_options.merge!(:dictionary => dict) if dict

      association_reflection = self.class.reflect_on_association(association)
      raise AssociationNotFoundException.new("#{self.class}##{association}") if association_reflection.nil?

      if options[:validate] == false
        kopy.instance_eval do
          # Force :validate => false on all saves.
          def perform_validations(options={})
            options[:validate] = false
            super(options)
          end
        end
      end

      cloned_object = send(
        "dup_#{association_reflection.macro}_#{association_reflection.class.name.demodulize.underscore.gsub('_reflection', '')}", 
        { :reflection => association_reflection, :association => association, :copy => kopy, :dup_options => dup_options },
        &block
      )

      kopy.send("#{association}=", cloned_object)
    end
  end

  return kopy
end