Single Table Inheritance

I have been experimenting with Single Table Inheritance as described in the More Active Record chapter in Agile Development with Rails.

I have started with an object model with three classes x, y and z (for brevity), where y < x and z < y. (I will want to extend this model further to subclass y with other classes.)

I have created a table called x (plural) in the database with attributes for x, y and z and a type attribute.

I then built a default scaffold application for x and added the subclasses y and z in the model x.rb. Using 'console (irb)' the behaviour is as expected, that is,

z.create(...) - creates a row in the table x with the type = 'z' and the attributes of x, y, z set accordingly.

This is not the case however when I use the scaffold-generated 'create' page. When using this to create rows in table x, the entered attributes (eg, x.a and z.b) are set correctly in the newly created table row but the 'type' is not set (to z). I assume I need to change either the controller or model to get the correct behaviour, and would appreciate some guidance as to how to approach this.

Thanks in advance.

<snip>

I then built a default scaffold application for x and added the subclasses y and z in the model x.rb. Using 'console (irb)' the behaviour is as expected, that is,

z.create(...) - creates a row in the table x with the type = 'z' and the attributes of x, y, z set accordingly.

This is not the case however when I use the scaffold-generated 'create' page. When using this to create rows in table x, the entered attributes (eg, x.a and z.b) are set correctly in the newly created table row but the 'type' is not set (to z). I assume I need to change either the controller or model to get the correct behaviour, and would appreciate some guidance as to how to approach this.

I am assuming you have a line X.new(params[:x]) in the scaffold-generated controller somewhere. The "type" field is set by AR based on the type of the instantiated model class.

You will need to write custom controller code that either conditionally instantiates one of x,y or z or create three separate controllers, if that is matches your business domain more closely.

Scaffolding is sometimes nice, but you will rewrite almost all of it anyway.

Cheers, Max

Max, thanks for taking time to respond.

I have parked STI for now - I did get it to work but wasn't happy with the impurity of the model ie. all attributes belong to base class, too many NULLs in base table etc. I can see it's value for certain class hierarchies but not the one I am working on - a number of z subclasses which inherit from a single y class which in turn inherits from a single x class, and where z subclasses may be very different from eachother.

The solution I now have in mind is the following:

1) One controller with a new() action that instantiates the base class (x) and correct subclasses (y,z) for the input form data (using a 'type' selected in the UI) 2) Create action that saves x and thru table association (e.g. has one, belongs to) saves the appropriate subclasses (y,z). 3) Individual models - one for x and y, and one for each sub class z.

I hope this'll work and is a sensible approach - further advice welcomed.

Thanks,

Lee.

(sorry if this ends up being a double-post, my first post was refused)

One thing I like to do in situations where the sub-types only require different attributes is use STI and encode attributes/content in YAML in a before_save. Then in after_find you can hook a method to YAMLize the in-record YAML and build up a nice model.my_attributes.key handler for it, and refer to the attributes in that way from your sub-classes.

The technique is easiest to understand via example, and for that I recommend you take a look at the open-source tumblelog engine project called Ozimodo; specifically inspect how it uses YAML to define post types and handle related attributes (there's no STI in this specific example, but you can combine the technique with STI if you insist).

Regards,

Thanks.

I think I understand :slight_smile:

Does this mean the STI database table for the base class (say 'x' where 'y' < 'x') does not need to have columns for attributes of 'y' or other subclasses?

For example,

def class X   attr_writer :name end def class Y < X   attr_write :number end

Using STI, define table for X with columns id, name, type and 'YAML container'.

Create instance of Y and before_save of X, encode Y (and thus X) attributes into 'YAML container'

On find, use 'type' to YAMLise 'YAML container' into appropriate object e.g. Y (but could be other sub classes). Use this object, ignoring object X.

What is meant by a key handler?

Yes, it means what you describe. Better yet, Rails can do this for you (look at 'serialize' for an attribute in the API docs). The technique I described however does some meta magic to also translate your_attribute.some_key to your_attribute[:some_key] which is what 'serialize' likes (since the serialized attribute is turned into a hash when the record is read).

-Bosko