The Actual Problem
I have different kinds of questionaires to build, but these questionaires have some parts in common. Therefore, I want to extract out these common parts, so that I can:
- Build the common parts at one place when rendering
- Put in some common functionality to my questionaires. For example, do user verification before starting the questionaire.
- Have one place to list all different kinds of questionaires, and by clicking them, I can go&visit each different questionaire.
The Abstracted Problem
I can have a base type, which contains the common informaiton about these questionaires; then for whatever questionaire I am adding in, I should be able to extend from that base questionaire type, therefore automatically have these common behaviours & common properties
When I save it into the database, the structure should be something like this:
BaseTable - name - age ... | TennisQuestionaireTable - base_id - tennis_ball_num - ... | PingPangnQuestionaireTable - base_id - pingpang_player_name - ...
The Single Table Inheritance
Rails has the Single-Table-Inheritance(STI) implemented: simply by adding the ‘type’ into the actual database table. This post has nicely summarized what is STI and how&why to use it. Let me paraphrase it a bit here (not quote from the blog post):
You have multiple models, but all are referring to the same table. The ‘type’ in that table tells which model would take control of this record. By having multiple model upon a single table, one can get more control over each different model types.
I personally see very little use of this STI mechanism: usually, if the model is different, then the corresponding database structure is likely to be different also (like here in my use case of questionaire).
The Multi Table Inheritance
I would define the multi-table-inheritance(MTI) this way:
You have a parent model, and you have several child models which inheritant from the parent model. Each model has their own table; although the child table don’t have the fields in the parent table, the child model can still use the records in the parent’s table, as if these fields exist in the child table. When these fields are updated by the child table, they’re updated in the parent table accordingly. More-over, one can get all the childrens of a parent, by simply querying upon the parent table.
Rails don’t have a native support for the MTI, therefore I need to implement my own. Here is how I implemented
Firstly, the base table. This defines the structure of the basic model, and other extending models would also extend form a module of this base table.
Correspondingly, in my extended model, I have the vary simple structure below:
The extended model would just stay simple, we’ll adding more methods in the base models to fully support the MTI.
Firstly, I want to be able to access the property of the base table, just like accessing the properties of my extended table. To make it short, I want functions like method below:
This is done using a method_missing overiding, to delegate the missing method to the base_table if cannot find in the extended table:
Secondly, I want to be able to get instance of the extended table, from the base table. This is done by saving the name of the extended_table:
In order to use it properly in the controller, esp. the methods of:
to be used in the controller, I need to override the
Up to this point here, the model has been very-much defined to support MTI, and it’s pretty clean – if one needs to create a new extended model, all one needs to do is to do:
and that’s all.
Now we need to change the controller part, which is a bit.. messy.
Firstly, we would extend from the BaseController, and in the filtering of the params, permit the base_table:
This is a bit unclean because it requires some actual changes, in a deep location, to the code to use the MTI. But that’s the only method I can come up with.
base_table_properties is defined in the
BaseController; it also defines the
update_param to pre-process
the params passed-in. The extended_name is the one to pass to the BaseController, so that it knows which key of
the params to set. Here’s hte
Up to this point, we’ve done model and controller part. Now let’s deal with the view.
Let’s create a common view for the base model:
# File: app/views/base_table/_form.html.slim = fields_for base_form do |base_form| .field = base_form.label :name = base_form.text_field :name .field = base_form.label :age = base_form.number_field :age
Now, whenever we need the base_table in the view, we can simply do:
# File: app/views/base_table/_form.html.slim = render 'base_table/form', base_form: @tennis_questionaire.base_table
and done. If one wants to add more common view into the
base_form, one can simply add these codes into
I’ve created a demo-project to further illustrate the idea. Here’s the link: https://github.com/flyfy1/rails-mti-demo
Further improvement on this mothod is to make it a gem.. it would then be (hopefully) easier to use.
Hopefully this helps someone :)