[Awesome Ruby Gem] Use deep_cloneable gem to clone active_record objects including associations and several operations

deep_cloneable

deep_cloneable gem gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations

Installation

You can install it as a gem:

1
$ gem install deep_cloneable

or add it into a Gemfile (Bundler):

1
2
3
4
5
# Gemfile

# GitHub - moiristo/deep_cloneable: This gem gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations.
# https://github.com/moiristo/deep_cloneable
gem 'deep_cloneable', '3.1.0'

Usages

The deep_clone method supports a couple options that can be specified by passing an options hash. Without options, the behaviour is the same as ActiveRecord’s dup method.

Association inclusion

Associations to be included in the dup can be specified with the include option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Single include
pirate.deep_clone include: :mateys

# Multiple includes
pirate.deep_clone include: [ :mateys, :treasures ]

# Deep includes
pirate.deep_clone include: { treasures: :gold_pieces }
pirate.deep_clone include: [ :mateys, { treasures: :gold_pieces } ]

# Disable validation for a performance speedup when saving the dup
pirate.deep_clone include: { treasures: :gold_pieces }, validate: false

# Conditional includes
pirate.deep_clone include: [
{
treasures: { gold_pieces: { if: lambda{|piece| piece.is_a?(Parrot) } } } },
mateys: { unless: lambda{|matey| matey.is_a?(GoldPiece) }
}
]

ship.deep_clone include: [
pirates: [ :treasures, :mateys, if: lambda {|pirate| pirate.name == 'Jack Sparrow' } ]
]

The Dictionary (Object Reusage)
The dictionary ensures that models are not duped multiple times when it is associated to nested models. It does this by storing a mapping of the original object to its duped object. It can be used as follows:

1
2
3
4
5
6
7
# Enables the dictionary (empty on initialization)
pirate.deep_clone include: [ :mateys, { treasures: [ :matey, :gold_pieces ] } ], use_dictionary: true

# Deep clones with a prefilled dictionary
dictionary = { mateys: {} }
pirate.mateys.each{|m| dict[:mateys][m] = m.deep_clone }
pirate.deep_clone include: [ :mateys, { treasures: [ :matey, :gold_pieces ] } ], dictionary: dictionary

Attribute Exceptions & Inclusions

The deep_clone method supports both except and only for specifying which attributes should be duped:

Exceptions

1
2
3
4
5
6
7
8
# Single exception
pirate.deep_clone except: :name

# Multiple exceptions
pirate.deep_clone except: [ :name, :nick_name ]

# Nested exceptions
pirate.deep_clone include: :parrot, except: [ :name, { parrot: [ :name ] } ]

Inclusions

1
2
3
4
5
6
7
8
# Single attribute inclusion
pirate.deep_clone only: :name

# Multiple attribute inclusions
pirate.deep_clone only: [ :name, :nick_name ]

# Nested attribute inclusions
pirate.deep_clone include: :parrot, only: [ :name, { parrot: [ :name ] } ]

Pre- and postprocessor

You can specify a pre- and/or a postprocessor to modify a duped object after duplication:

1
2
pirate.deep_clone(include: :parrot, preprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })
pirate.deep_clone(include: :parrot, postprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })

Note: Specifying a postprocessor is essentially the same as specifying an optional block (see below).

Note: Using deep_clone with a processors will pass all associated objects that are being cloned to the processor, so be sure to check whether the object actually responds to your method of choice.


Optional Block

Pass a block to deep_clone to modify a duped object after duplication:

1
2
3
pirate.deep_clone include: :parrot do |original, kopy|
kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id)
end

Note: Using deep_clone with a block will also pass the associated objects that are being cloned to the block, so be sure to check whether the object actually responds to your method of choice.


Cloning models with files

Carrierwave

If you are cloning models that have associated files through Carrierwave these will not get transferred automatically. To overcome the issue you need to explicitly set the file attribute.

Easiest solution is to add the code in a clone block as described above.

1
2
3
pirate.deep_clone include: :parrot do |original, kopy|
kopy.thumbnail = original.thumbnail
end

ActiveStorage

For ActiveStorage, you have two options: you can either make a full copy, or share data blobs between two records.

Full copy example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Rails 5.2, has_one_attached example 1
pirate.deep_clone include: [:parrot, :avatar_attachment, :avatar_blob]

# Rails 5.2, has_one_attached example 2
pirate.deep_clone include: :parrot do |original, kopy|
if kopy.is_a?(Pirate) && original.avatar.attached?
attachment = original.avatar
kopy.avatar.attach \
:io => StringIO.new(attachment.download),
:filename => attachment.filename,
:content_type => attachment.content_type
end
end

# Rails 5.2, has_many_attached example 1 (attach one by one)
pirate.deep_clone include: :parrot do |original, kopy|
if kopy.is_a?(Pirate) && original.crew_members_images.attached?
original.crew_members_images.each do |attachment|
kopy.crew_members_images.attach \
:io => StringIO.new(attachment.download),
:filename => attachment.filename,
:content_type => attachment.content_type
end
end
end

# Rails 5.2, has_many_attached example 2 (attach bulk)
pirate.deep_clone include: :parrot do |original, kopy|
if kopy.is_a?(Pirate) && original.crew_members_images.attached?
all_attachments_arr = original.crew_members_images.map do |attachment|
{
:io => StringIO.new(attachment.download),
:filename => attachment.filename,
:content_type => attachment.content_type
}
end
kopy.crew_members_images.attach(all_attachments_arr) # attach all at once
end
end

# Rails 6
pirate.deep_clone include: :parrot do |original, kopy|
if kopy.is_a?(Pirate) && original.avatar.attached?
original.avatar.open do |tempfile|
kopy.avatar.attach({
io: File.open(tempfile.path),
filename: original.avatar.blob.filename,
content_type: original.avatar.blob.content_type
})
end
end
end

Shallow copy example

1
2
3
pirate.deep_clone include: :parrot do |original, kopy|
kopy.avatar.attach(original.avatar.blob) if kopy.is_a?(Pirate) && original.avatar.attached?
end

Skipping missing associations

By default, deep_cloneable will throw a DeepCloneable::AssociationNotFoundException error when an association cannot be found. You can also skip missing associations by specifying skip_missing_associations if needed, for example when you have associations on some (but not all) subclasses of an STI model:

1
pirate.deep_clone include: [:parrot, :rum], skip_missing_associations: true

References

[1] GitHub - moiristo/deep_cloneable: This gem gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations. - https://github.com/moiristo/deep_cloneable

[2] deep_cloneable | RubyGems.org | your community gem host - https://rubygems.org/gems/deep_cloneable/

[3] GitHub - amoeba-rb/amoeba: A ruby gem to allow the copying of ActiveRecord objects and their associated children, configurable with a DSL on the model - https://github.com/amoeba-rb/amoeba

[4] GitHub - clowne-rb/clowne: A flexible gem for cloning models - https://github.com/clowne-rb/clowne

[5] Duplicating models with complex nested associations - CookiesHQ - https://www.cookieshq.co.uk/posts/duplicating-models-with-complex-nested-associations