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.save
call, but I haven’t verified whether sa -
:instantiate
doesn’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
:instantiate
option is only added tohas_one
associations, but the mixin could be extended to work withbelongs_to
as well.