Factory methods and scoping with ActiveRecord inheritance hierarchies
Wednesday, October 24th, 2007Today 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: STI, Rails, factory, with_scope