Factory methods and scoping with ActiveRecord inheritance hierarchies

Today I ran into a problem with a factory pattern for an ActiveRecord inheritance hierarchy and scoping: has_many association members were being created without references to the “parent” model instance.  For example, my parent class looks something like this:

class Post < ActiveRecord::Base
  has_many :attachments, :as => :attachee
end

In my AttachmentsController, I was trying to create an attachment like this:

@post.attachments.create(attributes)

This approach is bog standard Rails and works fine.  The next iteration involved subclassing the Attachment model class and making Attachment#new method be a smart factory returning an instance of a subclass based on the initialization attributes:

class Attachment < ActiveRecord::Base
  validates_presence_of :attachee_id

  def new(attributes, &block)
    if self == Attachment
      return AttachmentURL if attributes[:url]

      return AttachmentBlob if (attributes[:blob] or attributes[:file])

    end

    super

  end
end

class AttachmentURL < Attachment
end

class AttachmentBlob < Attachment
end

Exercising this code in the console showed the right behavior:

Attachment.new({:url => ‘http://cho.hapgoods.com’})   # => AttachmentURL

But things started looking a bit squirrely when I started to use associations: my validation for the presence of the attachee_id reference was failing:

@post.attachments.create({:url => ‘http://cho.hapgoods.com’})

How could Rails be leaving out the critical association reference?!?  It turns out that association methods like create use :create scoping to set attribute values.  In ActiveRecord::Base.initialize, the :create scope is examined and all scoped attributes are assigned values from the :create scope.  My problem was that the :create scope was being lost when I switched to a subclass in the Attachments#new constructor.  This is the Important Fact:  A scoped ActiveRecord model does not imply scoping for subclasses.

I solved the problem by “transferring” the scope from the base class to the subclasses.  It really wasn’t that complicated and I ended with a really spiffy pattern for subclassed AR model factories.  Here is what my factory method looks like now:

class Attachment < ActiveRecord::Base
  def self.new(attributes, &block)
    if self == Attachment
      klass = AttachmentURL if attributes[:url]
      klass = AttachmentBlob if attributes[:blob] or attributes[:file]
      raise ‘Unknown Attachment type’ unless klass
      klass.send(:with_scope, :create => (scope(:create) || {})) do  # Scope subclass to same create attributes as this class.
        return klass.new(attributes, &block)
      end
    end
    super
  end
end

nota bene: It so happens that I was using Single Table Inheritance (STI) for this hierarchy, but I suspect the problem and solution would be identical with true abstract classes and arbitrary table mapping.  Also, the fact that I was using a polymorphic has_many association is almost certainly irrelevant.

Technorati Tags: , , ,

Leave a Reply

You must be logged in to post a comment.