See Also: ForumExample, ConcreteTableInheritance
Inheritance hierarchies can be represented in a relational database with at least three different forms. Martin Fowler describes the different forms with these short summaries in Patterns of Enterprise Application Architecture
1 http://www.martinfowler.com/eaaCatalog/classTableInheritance.html
2 http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
3 http://www.martinfowler.com/eaaCatalog/concreteTableInheritance.html
Active Record uses approach number two, which means that you have to add a string column — by convention named ‘type’, can be overridden using inheritance_column() — to the table where you want to keep the hierarchy. (Note that the column type must be a string type, a MySQL enum does not work.)
For more details see API docs: http://api.rubyonrails.org/classes/ActiveRecord/Base.html
Alternatively, some work on getting approach 1 working in Rails is here: ClassTableInheritanceInRails
If you have an “uninitialized constant” error:
Note: Rails must ‘see’ the file containing the STI classes before it can use them, otherwise you’ll end up with an “uninitialized constant” error. This can be a problem if the name of the class and the name of the file defining it are not the same, e.g. you have the model ‘Manager’ in the file ‘employees.rb’. Rails will not be able to divine the filename from the class name in this case.
An easy way to do fix this is to add “model :employees” to your application.rb controller, where ‘employees’ is the name of the file containing the STI class minus the extension (so in this case, the model would be contained in a file named ‘employees.rb’.) This forces Rails to load that file and ‘see’ all the models you have defined in it, and it will then know of your STI classes when you go to use them. {color:red} Using ‘model’ has been deprecated in Rails 1.2 (possibly earlier) use ‘require_dependency’ instead of ‘model’. An alternative to using “model :mymodel” is to just require the file defining the models used in the STI, eg. require ‘mymodel’
You may also get an “uninitialized constant” error if you have mistyped the name. For instance:
class Fastrabbit < Animal; end
# versus
class FastRabbit < Animal; end
Additional info & tips
People.find(:all)
Employees.find(:all)
first, in your controller:
# simplified
def new
case @params[:person_type]
when "Manager"
@person = Manager.new
when "Slave"
@person = Slave.new
end
end
then in your view, do:
<%= hidden_field_tag "person_type", @person[:type] %>
|{background:#ddd}. How does the view know which persontype to send with the form data?
-JamesH_ |
then in your controller’s create method:
#again, simplified
def create
case @params[:person_type]
when "Manager"
@person = Manager.new(@params[:person])
when "Slave"
@person = Slave.new(@params[:person])
end
if @person.save
redirect_to :action => :list
else
render_action :new
end
end
you could even go so far as to create a factory class method in your Person model
class Person < ActiveRecord::Base
def self.factory(type, params = nil)
case type
when "Manager"
return Manager.new(params)
when "Slave"
return Slave.new(params)
else
return nil
end
end
end
A more scalable version of the above could go something like below. The version below will raise an exception if the named subclass doesn’t exist (which could be considered a positive feature, depending on circumstance). You could also check for undefined subclasses and return Person if the subclass isn’t found:
class Person < ActiveRecord::Base
def self.factory(type, params = nil)
params[:type] ||= 'Person'
class_name = params[:type]
# if defined? class_name.constantize
class_name.constantize.new(params)
# else
# Person.new(params) # or do what you will...
# end
end
end
then in your controller’s new and create methods, you can do
def new
@person = Person.factory(@params[:person_type])
end
def create
@person = Person.factory(@params[:person_type], @params[:person])
if @person.save
...
end
end
Q: I tried to find a way to redefine Person.new instead of having to remember to use Person.factory everywhere, but couldn’t find a way to do it. Anyone?
A: Sure. Just put it in the ‘initialize’ method of the class. Class.new creates the new object then Class#initialize is called on that object.
person_type = "Manager" @person = eval(person_type + ".new")
person_type = "Manager" @person = person_type.constantize.new
WARNING: if person_type comes from an HTTP request, this is a security hole
person_type = "Manager" @person = person_type.constantize.new
#Lets say :type contains the string "Manager" type = params[:type] || "Article" if (type.constantize.base_class) == Person @person = type.new end
Another way of doing the above factory that fixes the security problem is to follow the convention that all created classes be prefixed by a standard name. That way the system won’t be instantiating unintended classes. Also, you need to use rescue instead of defined? to handle the exception raised by constantization of undefined class types. Like this:
def self.create(params = nil)
params[:person_type] ||= 'Person'
class_name = "Person#{params[:person_type]}"
begin
class_name.constantize.new(params)
rescue
Person.new(params)
end
end
A nice one-liner to check your constantization and return the model given you want your model’s superclass to be the passed in superclass is:
def check_valid_model(model_string, superclass)
(_model = model_string.constantize).superclass == superclass && _model
rescue
false
end
class ETH < ListData; end Ethnicity = ETH
Or you could create some accessors to the :type attribute to use in your forms.
def class_type=(value)
self[:type] = value
end
def class_type
return self[:type]
end
This way you can assign the type using things like a radio button.
radio_button("market", "class_type", "Exclusive")
Can someone post details on how to go about testing Single Table Inheritance models in Rails?
Ex: you have People, and Employee < People.
Do you create unit tests for both your People and Employee models? I’ve done this, have set the inheritance in the Employee model, as well as set_table_name “people”.
When I run ‘ruby unit/employee_test.rb’ it gives an error like:
‘myapp_test.employees’ doesn’t exist
How do I tell it to look at the People table instead?
To test STI classes, create fixtures in the baseclass fixture-file (’people’) and specify ‘type: Employee’ manually (note upper casing of ‘Employee’. The type column is case-sensitive, so make sure to use the UpperCase notation, not the rails_file notation when setting the class.)
I read that above statement about 10 times without getting the point: do not use fixtures :employees, :people or sth like that. There can’t be a fixture for employees; use fixtures :people, no more.
Also note that when you do the above, accessing fixtures’s methods will not work if you have cross-namespace STI. For example, if you have MyNameSpace::MyInheritingModel < MyModel
models.yml
--------
one:
id: 1
...data...
type: MyInheritingModel
You cannot use a method of models(:one), like models(:one).id
controller_test.rb
--------
fixtures: models
...
def test
model_one_id = models(:one).id # gets you ActiveRecord::SubclassNotFound
model_one_id = 1 # OK to do manual id
end
——
Gang, this’ll save you some hours of frustration. Your controllers may not see your STI subclasses unless you include the following in your controller files:
require_dependency ‘model’…where ‘model’ is the name of the parent class.
——
I’ll add to the above – while refactoring to use STI, rails wouldn’t even start – turns out I had an observer looking for a class that didn’t have a corresponding file name (Host was defined inside ‘asset.rb’); adding
require_dependency ‘asset’
to to the top of the observer was what was needed (that was hours of fun).
——
Note also that Rails doesn’t set the [:type] field for the base class in Single Table Inheritance. Only subclasses of the base class have the type field filled in.
——
Q. What about model attributes when using STI?
eg. I have Page, Article and Comment all inheriting from ContentItem. Only comment uses the commentable_id column.
Some of these models will have more attributes than others. How do I make sure the more specific atrributes are hidden or disabled from the parent model, while enabling those attributes for select models using those attributes?