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