Rails Mixin: Auto-instantiated Associations
- ruby
- rails
- mixins
Often times you have an association that you want to be available as soon as you access it. That is, given a User
model that has_one :location, you want to call user.location.country without worrying about whether user.location
has been instantiated.
You could delegate the fields you are interested in to the location object and allow_nil, but you might want to
assign the association’s attributes:
user = User.new(...) user.location.country = 'GB' user.save
Delegation to a nil object won’t help you out in this case.
You could conditionally instantiate the association in an after_initialize callback, but you might not always want
to create the associated record. Write enough after_intialize callbacks to do this and they’ll start looking a lot
like boilerplate code - why not specify the behavior on the association itself?
The mixin
Fortunately, Rails’ singular associations have a simple path to load the associated record:
ActiveRecord::Associations::SingularAssociation#load_target. This is specific to Rails 3 (or more precisely, to
ActiveRecord 3.2.x), but likely applies to newer versions of Rails as well.
First, we’ll need to enhance SingularAssociation#load_target to instantiate the associated
record when it doesn’t exist:
module LoadTargetWithInstantiation
def load_target
super || if options[:instantiate]
record = build_record({}, {})
if owner.persisted?
record.save or raise RecordInvalid.new(record)
end
@target = record
loaded! unless loaded?
set_new_record(record)
record
end
end
end
ActiveRecord::Associations::SingularAssociation.send(
:prepend, LoadTargetWithInstantiation
)
The original load_target will return nil if the associated record doesn’t exist (either in the database, or in
memory prior to persistence).
Then, we just need to make sure that we can specify an instantiate option on our associations:
ActiveRecord::Associations::Builder::HasOne.valid_options += [ :instantiate ]
Now we can make our example work:
class User < ActiveRecord::Base
has_one :location, instantiate: true
end
# ...
user = User.new(...)
user.location.country = 'GB'
user.save
The caveats
-
For persisted owner records, accessing the associated record will instantiate and persist it. This behavior could easily be modified by removing the
record.savecall, but I haven’t verified whether sa -
:instantiatedoesn’t necessarily imply:autosave. There are some situations where changes to the associated model might not be persisted when the owner model is saved. -
The
:instantiateoption is only added tohas_oneassociations, but the mixin could be extended to work withbelongs_toas well.