Home

Jan 31

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 to has_one associations, but the mixin could be extended to work with belongs_to as well.



blog comments powered by Disqus