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. 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:
Additional info & tipsclass Fastrabbit < Animal; end # versus class FastRabbit < Animal; end
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] %>
| How does the view know which person_type 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. Whenever Class.new is called Class.initialize is too.
person_type = "Manager" @person = eval(person_type + ".new")
person_type = "Manager" @person = person_type.constantize.new
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.