ActiveRecord.dup
and constantize
July 16, 2020 ( last updated : July 17, 2020 )
active-record
ruby
I’m still unsure how I want to use this blog and wether I intend on creating a separate section for these short “TIL” posts or if I want to keep them here, but for today I’m combining two TILs into a longer post. I’m also using this to experiment with a post not having an image associated with it; we’ll see if that bugs me and I end up changing it or not.
This week I’ve learned a bit about the limitations ActiveRecord’s dup
function as well as how constantize
works.
dup
methodThis method creates a new record that is a duplicate of whatever record you’re calling it on. Unfortunately it intentionally doesn’t duplicate your associations so you’ll need to duplicate those manually.
From the documentation:
…copies the object’s attributes only, not its associations. The extent of a “deep” copy is application specific and is therefore left to the application to implement according to its need.
Example
For a setup like this:
class ParentObject < ActiveRecord::Base
has_many :child_objects
attr_reader :name
end
class ChildObject < ActiveRecord::Base
belongs_to :parent_object
attr_reader: name
end
parent = ParentObject.create!(name: 'Jimbo Jones')
kearney = ChildObject.create!(name: 'Kearney Zzyzwicz')
parent.child_objects << kearney
lisa = ChildObject.create!(name: 'Lisa Simpson')
parent.child_objects << lisa
dolph = ChildObject.create!(name: 'Dolph Starbeam')
parent.child_objects << dolph
If we wanted to duplicate the record and it’s associations we would need to do duplicate each child object like this:
new_parent = parent.dup
new_children = parent.child_objects.map(&:map)
new_parent.child_objects << new_children
new_parent.name == parent.name # => true
new_parent.id == parent.id # => false
new_parent.child_objects.map(&:name).sort == parent.child_objects.map(&:name).sort # => true
new_parent.child_objects.map(&:id).sort == parent.child_objects.map(&:id).sort # => false
NOTE: dup
does not duplicate the values in the timestamps.
constantize
assumes top-level constantWhile writing a service that programmatically retrieves constants living at the class level, I wrote something that looks like this:
class MyAwesomeService
TYPE_ONE_CONSTANT = 'type_one'.freeze
TYPE_TWO_CONSTANT = 'type_two'.freeze
TYPE_THREE_CONSTANT = 'type_three'.freeze
def initialize(constant_type_name)
@value_from_constant = "#{constant_type_name.upcase}_CONSTANT".constantize
end
def execute!
puts "The value for the constant you selected was #{@value_from_constant}"
end
end
MyAwesomeService.new('type_one') # => NameError: uninitialized constant TYPE_ONE_CONSTANT
This is because constantize
assumes the constant is at the top-level:
"TYPE_ONE_CONSTANT".constantize # => ::TYPE_ONE_CONSTANT
Our implementation actually implements that constant in the MyAwesomeService
namespace: MyAwesomeService::TYPE_ONE_CONSTANT
So instead, if we wanted to retrieve this constant we would could use either self.class.const_get()
or prepend the expression with MyAwesomeService::
constant_type_name = 'type_one'
self.class.const_get("#{constant_type_name.upcase}_CONSTANT") # => MyAwesomeService::TYPE_ONE_CONSTANT
# or
"MyAwesomeService::#{constant_type_name.upcase}_CONSTANT".constantize # => MyAwesomeService::TYPE_ONE_CONSTANT
Since the time I wrote this I’ve updated the implementation so it doesn’t require this kind of pattern, but knowing the way the assumptions constantize
makes is very useful.