Using Single Table Inheritance (STI) models with Hobo

Posted by Bryan Larsen.

Here’s what I needed to do to get STI (Single Table Inheritance) working. It’s fairly straightforward, but there are a couple of gotchas. Thus, this recipe.

The first gotcha is that you have to have your base table fully generated and migrated before you generate the child model. See Bug 345.

In my case:

script/generate hobo_model_resource DownloadedFile filename:string contents:string
script/generate hobo_migrations

I chose the “m” option for hobo_migrations, so the database was migrated. Then once I had the base class working:

script/generate hobo_model_resource BatchAcknowledge

I then edited downloaded_file.rb and added sti_type :string to the fields block and added set_inheritance_column :sti_type below the fields block.

I then edited batch_acknowledge.rb to change its parent from ActiveRecord::Base to DownloadedFile. I then removed everything from the file, included the fields definition and the permissions checks – it gets those from the base class.

script/generate hobo_migrations

If you want to add additional fields to your sub-models, you have to add them to your base model. This is how ActiveRecord Single Table Inheritance work. However, validations and association definitions may be added to the child model. In my case, I added a field submission_id to the base class, and added:

belongs_to :submission
validates_existence_of :submission

to the child class.

Now you should have working inherited models. You’ll notice that forms and pages will be generated for BatchAcknowledge. Cards are not, but that’s fairly irrelevant, because the DownloadedFile card will display a BatchAcknowledge record, and the auto generator would have generated the two cards identically anyways.

In another small gotcha, the name may not propogate correctly for you: see Bug 387

A larger gotcha is that problems have been reported if you drop the database and try to run hobo_migrations: Bug 397

User contributed notes

  • On March 26, 2009 txinto said:

    Hi! Thanks for your recipe! I have a question: is it possible to redefine the fields of a child class?

    An example: I have a parent class called Node and has the total amount of fields (attributes) of their childs. I have two subclasses: ValueString and ValueDouble. ValueString has an attribute called valueStr, and ValueDouble has three: valueDb, valueMin, valueMax.

    As I am using STI, node has fields:name, type, valueStr, valueDb, valueMin, valueMax. I want to define a function in Node that lists all the attributes, and I want the correct results for each instance. If v1's type is ValueString, then v1.show_my_attributes must return [name, type , valueStr], if v2's type is ValueDouble then v2.show_my_attributes must return [name, type, valueDb, valueMin, valueMax].

    I have tried to re-define the fields in each model (the block "fields do ... end"), but I can not find a method to get only the attributes that are defined in the "fields do ... end" of each subclass.

    I have "avoided" the problem adding a method on node that lists explicitally all the fields (this is making the work twice), and overloading this method on each subclass, but I think there must be a way to do it.

    Any ideas?

    Thank you!

    Tx.
  • On April 23, 2009 Bryan Larsen said:

    Sorry, I didn't see your comment. We need to set up rss feeds for recipe comments!

    "STI" stands for "single-table", which means that all attributes are present in a single table.

    I think that you may be better off using an abstract base class instead of STI.
  • On May 04, 2009 Spiralis said:

    Thanks for the recipe. I have a typo warning and a problem to report.

    1. Typo: I have to use script\generate hobo_migration, not the plural "hobo_migrations"
    2. Problem: I have a base-class as proposed (Contract), generated and migrated, ready top use. I then create two additional models, that are to be the STI subclasses (CustomerContract and ArtistContract). I change the declaration for both of these models as proposed from AR to Contract. I also remove all the statements from the sub-classes, so that they are empty. All in all they look like this:

    class Contract < ActiveRecord::Base
    hobo_model
    fields do
    sti_type :string
    content_type :string
    filename :string
    timestamps
    end
    set_inheritance_column :sti_type

    def create_permitted?
    acting_user.administrator?
    end

    def update_permitted?
    acting_user.administrator?
    end

    def destroy_permitted?
    acting_user.administrator?
    end

    def view_permitted?(field)
    true
    end
    end

    class CustomerContract < Contract
    end

    class ArtistContract < Contract
    end

    When I then try to migrate I get continuous questions to confirm if I want to drop the Contract-fields (sti_type, content_type, filename and the timestamp variants). It obviously picks up having the fields from before, but that I want them removed when I subclass, somehow. In any case the response was not what I was expecting according to the recipe.
  • On May 05, 2009 oillio said:

    Spiralis,
    I ran into the same problem. It seems to be because the inheritance column is not yet defined in the database (may be associated with bug 345?).

    To work around, run your hobo_migration after you setup your inheritance column in the parent, but before your hobo_model_resource generation for your child.

    I also have a bonus question: The hobo_migration wants to set :default => nil on the inheritance column. Is there any way I can disable this? I want to set my own default.
  • On May 05, 2009 Spiralis said:

    Thanks. That brought me one step further. However, my next step was to add belongs_to specifiers for the sub-classes models, and migration doesn't seem to pick them up. Kinda rhymes with the warnings about field-specs needing to be in the base. But, how would I add a belongs_to in the base-class, that is only supposed to belong_to for a given sub-class. Since both my sub-classes need this, then the base-class will actually need two extra belongs_to declarations, and only one of them will be used for each record:

    class Contract < ActiveRecord::Base
    hobo_model
    fields do
    sti_type :string
    content_type :string
    filename :string
    timestamps
    end
    set_inheritance_column :sti_type

    belongs_to :artist #only to be used for ArtistContract base-class
    belongs_to :customer #only to be used for CustomerContract base-class

    # snip...
    end

    Normally I'd place the two belongs_to above in the sub-classes. Is this a hobo-problem or is that not allowed in Rails STI?

    I don't have an answer to your bonus-question, and I won't be having the same problem neither since I will never need to instantiate the base-model. I will always use the sub-classes, so the type field will be initialised.
  • On May 05, 2009 Spiralis said:

    I have fixed the problem for my case by not using STI. Instead I am using polymorphs to allow for my artists and customers to be attached to the contracts. I was hesitating going down that path, but that really went well to be honest.

    The only problem left is that hobo somehow fails in the GUI for my contracts when trying to edit or create a new record. It claims to be missing the "changed?" method. I guess this is part of the permission checks and polymorphism might be confusing the view-generator?
  • On May 06, 2009 oillio said:

    To use belongs_to in a child class I would try creating the foreign key manually (in the fields block). I have not tried it myself but I do not believe Hobo is smart enough to set this association up automagically.

    But I think polymorphs are probably the way to go for you if that association is the only difference between your subclasses.