Fork me on GitHub



Ruby Class Inheritance &
Multiple Table Inheritance Embeddings :

A full featured solution
based on Ruby Class Inheritance & Views



ALTRABio, February 11th 2011   

The CITIEsForRails gem is delivered

by ALTRABio under MIT License  

Authors : L. Buffat, P.E. Jouve      

Contact : citiesforrails”the_at_sign”gmail.com





           When trying to model a real world problem that needs some information storage into databases, programmers often face the issue which consists in mapping objects to relational databases (see for instance here). After a short (and not exhaustive) review of Rails existing solutions we propose a new solution which has been bundled into a gem called : CITIEsForRails Class Inheritance & Table Inheritance Embeddings For Rails.

The form of this article is deeply inspired by Multiple Table Inheritance with ActiveRecord by Maxim Chernyak

1. Problem Statement :

Imagine writing a multimedia library app with different types of medias (for example : Audio, Mp3, Video, Book, Novel, Dictionary, Pocket_Dictionary, and Unknown medias) .

Normally all Medias would have common attributes such as name and price.

Some attributes will likely differ :

This inheritance looks reasonable, but now we have to come up with relational database structure. We need to find a way to store each class’s own attributes (Audio’s own attributes, Mp3's own attributes, Video's own attributes... as well as their common (Media’s) attributes without duplication).

We will first review solutions at our disposal,and also shortly discuss their drawbacks. Then we will introduce a new solution which to our mind eliminates drawbacks and extends the features of existing solution.



2. Existing Solutions

2.1. Database Management System based solution

Some relational database management systems RDBMS (such as PostgreSQL) provide support for table inheritance (see here), but it’s a specialized feature which ties you down to the given RDBMS.

2.2. Single Table Inheritance based solution

ActiveRecord provides only one way to handle a is_a relationship which is Single Table Inheritance (see also here). You’d have to create a table looking somewhat like the following.

id

type

price

name

title

genre

author

language1

language2

summary

1

Audio

10

n1

t1

g1

NULL

NULL

NULL

NULL

2

Mp3

5

n2

t2

g2

NULL

NULL

NULL

NULL

3

Video

11

n3

t3

g3

NULL

NULL

NULL

NULL

4

Book

12

n4

t4

NULL

a4

NULL

NULL

NULL

5

Novel

13

n5

t5

NULL

a5

NULL

NULL

s5

6

Dictionary

10

n6

t6

NULL

a6

l16

l26

NULL

7

PocketDictionary

7

n7

t7

NULL

a7

l17

l27

NULL

8

Unknown

5

n8

NULL

NULL

NULL

NULL

NULL

NULL

The problem here is that all attributes are stored in the same table. It’s likely that soon the number of attributes will grow unmanageable, and most of them will always stay NULL since they’ll be specific to only one type (not really efficient storage).

2.3. Polymorphic Has_one Association based solution

A has_one association allows us to split out medias, audios, mp3s, videos, books, dictionaries, pocket dictionaries, novels and unknowns into different tables. We won't deeply explain drawbacks of this solution but only point out that it creates a “has_a” relationship whereas we want a is_a relationship.

2.4. Multiple Table Inheritance (Simulated) based solution

Has_one association problem is that it creates a “has_a” relationship, and we want a “is_a”. Since there isn’t much choice, several authors (see Multiple Table Inheritance (MTI), or Class Table Inheritance (CTI)) decided to make it look like we have an “is_a” relationship. We won't discuss here implementation details and you should refer to the previously given links for more information. But, you should be aware that for these solutions all subclasses would inherit from ActiveRecord::Base and that these solutions consist in doing some clever things to fake some aspects of classes inheritance but not all (for instance, if you use this solution, calling Book.superclass would return ActiveRecord::Base and not Media as we may have desired)

Nevertheless, their authors have done a pretty good work, and solutions proposed would fit to a lot of inheritance cases and would provide users of their plugins with a functional solution to their problems. Yet, in some cases (that we enumerate below) this won't be enough.

Flat Class Hierarchy / One level Class Hierarchy :






3. A New Solution : Embedding Ruby Class Inheritance & Multiple Table Inheritance through use of Ruby Class Inheritance & Databases Views

As for now, we have seen that except the Database Management System based solutions which may be based upon a special feature of PostgreSQL RDBMS (and which intrinsically rely on the use of PostgreSQL) there is no completely satisfying solution for solving our class inheritance and multiple table inheritance problem. We have recently decided to create a plugin that would allow solving such a problem. Our solution named CITIEsForRails was designed after a close look at the existing solutions and we want to thank here their authors for letting their code at community's disposal. We won't get here into a deep technical explanation of how our solution works but just give some very low level insights (maybe in near future we'll provide some more details ; yet, interested reader should refer to our commentated code, which can be grabbed here). Thereafter, we first introduce features offered by our solution, give some brief informations related to the basic ideas that drove us for implementing this solution, and finally explain how to use it. Obviously, the explanations concerning the implementation of this solution may be not clear enough, but you could still skip them and still use our gem efficiently. Maybe, the best idea to understand completely the job done would be to investigate the commentated code.

3.1. A Full Featured Solution

3.2. Solution Philosophy at a glance

The philosophy used to implement this solution is the following, for each node of the class hierarchy (see it here) (does not depend on the fact that it has or not any specific attribute) we would like to use ActiveRecord facilities to manipulate it (this means using ActiveRecord facilities to save an object, update an object, load an object, read an object's attributes, destroy an object...). Using classically ActiveRecords would imply to use a specific table for each class, yet this would not respect the constraints required related to the storage. (this would not fit with our database schema constraints defined before, see it here).

So what we propose here is just to have a mechanism that fake the existence of these tables while not requiring them. Our solution is actually based on a database schema such as the one needed.

The idea is basically to use the database schema proposed and to associate one/several defined tables for each class node of the hierarchy (this tables will be used for saving/storing/updating informations for objects) and to associate to each class node one virtual table (database views) or a table that will be accessed for loading/reading informations. Then the idea is to override some ActiveRecord methods in order to propose to the user a classical use of ActiveRecord functionalities. The overriding will in fact consists in switching from the virtual table (the view) to the real tables whenever it is needed. You need the virtual table for loading/reading informations and the real tables for saving/updating informations. This is not clear ? Hopefully the use of the gem does not need any good understanding of how it works, and furthermore won't change your habits as far as the use of ActiveRecord functionalities are concerned. Yet I just give know some simple drawings that try to illustrate what happen behind the curtains when using our gem :

See class hierarchy here

See database schema here

Database Views


Class Hierarchy & Associated Tables / Views






3.3 CITIEsForRails User's Guide

We give here the details to use our solution. Note that a functional web app implementing the multimedia library example is available here. It may be worth having a look at it since each specific code entry necessitated by the use of our plugin has been associated with a “#needed for CITIEsForRails” comment. Moreover some extra Comments & BookComments objects have been added to exhibit that Callbacks functionalities are also available (in fact every ActiveRecord facility should be available). Installing this app and getting into code may be a good way to understand how to use our gem.

You should follow the next steps to use CITIEsForRails :

  1. Model your class inheritance hierarchy (see for instance our example)

  2. Once your class inheritance hierarchy is modeled : determine what we will call the role of each class. There are 3 kind of roles :

  1. Once roles are determined : determine the database schema (it is better if this schema is such that it avoids duplication (see for instance our example). Note that the table associated to the mother class requires one extra columns that we will call inheritance_column (our solution uses notably Rails Single Table Inheritance facility). (To sum up : the “mother class” necessitates a table with columns : id, inheritance_column and a column per attribute ; each ”class with specific attribute(s)” necessitates a table with columns : id and a column per specific attribute).

    For our example, this mean that we need 6 tables :

  1. Determine views that are needed (one per ”class with specific attribute(s)”) (see for instance our example) . Such views may be created thanks to the provided function CreateTheViewForCITIEs(class) or dropped with the provided function DropTheViewForCITIEs(class) . For our example, this mean that we need 5 views :

  1. Write the migrations needed for establishing the database schema (tables & views)

    For our example this would mean generating the following migrations :

    class CreateMedias < ActiveRecord::Migration

    def self.up

    create_table :medias do |t|

    t.string :inheritance_column_name

    t.string :name

    t.integer :price

    end

    end

    def self.down

    drop_table :medias

    end

    end

    #--------------------------------------

    class CreateBooks < ActiveRecord::Migration

    def self.up

    create_table :tablebooks do |t|

    t.string :title

    t.string :author

    end

    CreateTheViewForCITIEs(Book)

    end

    def self.down

    DropTheViewForCITIEs(Book)

    drop_table :books

    end

    end

    #--------------------------------------

    class CreateDictionaries < ActiveRecord::Migration

    def self.up

    create_table :dictionaries do |t|

    t.string :lf

    t.string :lt

    end

    CreateTheViewForCITIEs(Dictionary)

    end

    def self.down

    DropTheViewForCITIEs(Dictionary)

    drop_table :dictionaries

    end

    end

    #--------------------------------------

    class CreateNovels < ActiveRecord::Migration

    def self.up

    create_table :novels do |t|

    t.string :summary

    end

    CreateTheViewForCITIEs(Novel)

    end

    def self.down

    DropTheViewForCITIEs(Novel)

    drop_table :novels

    end

    end

    #--------------------------------------

    class CreateVideos < ActiveRecord::Migration

    def self.up

    create_table :videos do |t|

    t.string :title

    t.string :genre

    end

    CreateTheViewForCITIEs(Video)

    end

    def self.down

    DropTheViewForCITIEs(Video)

    drop_table :videos

    end

    end

    #--------------------------------------

    class CreateAudios < ActiveRecord::Migration

    def self.up

    create_table :audios do |t|

    t.string :title

    t.string :genre

    end

    CreateTheViewForCITIEs(Audio)

    end

    def self.down

    DropTheViewForCITIEs(Audio)

    drop_table :audios

    end

    end

    #--------------------------------------

  2. Now you have to indicate for each classes its role. This is done into your models definitions by adding the special phrase : “acts_as_cities” only for the mother_class and for each ”class with specific attribute(s)”. This special phrase may be accompanied by an optional option hash : {:table_name=>'name_of_the_table_associated_with_the_class' , :db_type_field=>'name_of_the_inheritance_column'}. Note that default values for these options are respectively the pluralized name of the class for :table_name and 'type' for :db_type_field.

    For our example this would imply the following :

    class Media < ActiveRecord::Base

    acts_as_cities #default values are used there since the table associated to Media class is medias and the inheritance column is named type

            #in the following models definitions we won't mention that the default value is used for :db_type_field

            #and will only mention non default value for :table name when needed

    end

    #--------------------------------------

    class Audio < Media

    acts_as_cities

    end

    #--------------------------------------

    class Mp3 < Audio

    end

    #--------------------------------------

    class Book < Media

    acts_as_cities :table_name=>'tablebooks' #here we need to specify :table_name=>'books' since the table associated to Book class is tablebooks and not book

    end

    #--------------------------------------

    class Dictionary < Book

    acts_as_cities

    end

    #--------------------------------------

    class Novel < Book

      acts_as_cities

    end

    #--------------------------------------

    class PocketDictionary < Dictionary

    end

    #--------------------------------------

    class Unknown < Media

    end

    #--------------------------------------

    class Video < Media

    acts_as_cities

    end

    #--------------------------------------

  3. And that's all...

  4. Well, not really, you will also have to install our gem thanks to the following command

      gem install CITIEsForRails

We hope these explanations will be enough for you to use our gem...



    WARNINGS

    Whenever you modify a table (adding removing columns) please consider (if necessary) to update the (eventually) corresponding view. This can be done by successively Dropping and re-Creating the view (thanks to the DropTheViewForCITIEs and CreateTheViewForCITIEs methods)

    Some more documentation to come....

      

      

        

    -----------------------------------------------------------------  

    We encourage you to :

try

test

use

enhance

document this gem...

          

      

    Give us some feed back at :

    citiesforrails”the_at_sign”gmail.com

          

    -----------------------------------------------------------------