If you've decided to use ActiveRecord's Single-Table Inheritance (STI) and you want it to be easy for subclasses to have arbitrary additional attributes, you might be tempted to create an Attribute model and do something like this (there are optimizations to be made, but I'm not bothering):
class StiSuperclass < ActiveRecord::Base
has_many :attributes
def method_missing(name, *args)
name = name.to_s
setter = /=$/ === name
expected_args = 0
if setter
name = name[0...-1]
expected_args = 1
end
unless args.size == expected_args
err = "wrong number of arguments (#{args.size} for #{expected_args})"
raise ArgumentError.new(err)
end
attribute = attributes.find { |a| a.name == name }
if setter
if attribute
attribute.value = args[0]
else
attribute = Attribute.new(:name => name, :value => args[0])
attributes << attribute
args[0]
else
attribute ? attribute.value : nil
end
end
end
class Attribute < ActiveRecord::Base
belongs_to :sti_superclass
serialize :value
end
While that can work, it's inefficient both in terms of database usage and execution speed. Since these attributes don't have any real meaning in the database to begin with, it's much simpler and easier to put them in a column of the base table. Consider this alternate design:
class StiSuperclass < ActiveRecord::Base
serialize :attributes
def method_missing(name, *args)
setter = /=$/ === name.to_s
expected_args = 0
if setter
name = name.to_s[0...-1].to_sym
expected_args = 1
else
name = name.to_sym
end
unless args.size == expected_args
err = "wrong number of arguments (#{args.size} for #{expected_args})"
raise ArgumentError.new(err)
end
if setter
attributes[name] = args[0]
else
attributes[name]
end
end
private
def attributes
self[:attributes] ||= {}
end
def attributes=(hash)
attributes.replace(hash)
end
end
We now have no need for an additional table and lots of joins. Better yet, we can make our additional attributes explicit in the subclasses and do away with method_missing with the following:
class StiSuperclass < ActiveRecord::Base
serialize :attributes
def self.additional_attribute(*names)
names.each { |name|
name = name.to_sym
define_method(name) { attributes[name] }
define_method("#{name}=") { |value| attributes[name] = value }
}
end
private
def attributes
self[:attributes] ||= {}
end
def attributes=(hash)
attributes.replace(hash)
end
end
class StiSubclass < StiSuperclass
additional_attribute :foo, :bar
end
For those of you who are skeptical about the usefulness of arbitrary additional fields, consider it in the context of my current project. I am developing a framework for writing multiplayer card game backends. The Game model cannot (and should not attempt to) take into account the attributes any possible card game will need. Instead it provides a convenient hook for explicitly defining the additional attributes a particular backend will need.
Enjoy!
Labels: Metaprogramming, Rails, Ruby