Split a class (with existing data) with single table inheritance

December 5, 2007

Let’s say that i have a table called Person, with a corresponding database table ‘people’, that already has a lot of records in it. It so happens that in my app men and women have slightly different rules associated with them, and are often searched seperately (eg search boxes have a ‘only search women’ checkbox).

The database represents gender by a field called ‘male’, which holds a boolean: men are ‘true’ and women are ‘false’. (this seems a bit sexist, but it ensures that there are only two possible values).

I’m sick of typing

:condition => [“male = ?”, person.male]

all the time, and i’m generally sick of dealing with Person when there really should be two seperate classes which are dealt with differently. I want instead to be able to deal with Man and Woman, eg to sayMan.find(:all) etc, but i don’t want to start making serious changes to the database schema.

We can fix this with single table inheritance. This has two stages: 1) alter the database, and 2) extend Person into two new classes.

Stage 1 – we create a new string field in the ‘people’ table called ‘type’. This is simple with a database migration: at the command line (making sure we’re inside our project directory), type

ruby script/generate migration add_type_to_people

This should create a migration with that name.

Now, this migration has to do a bit more work than usual. We’re adding a new column, but we need to populate it as well, with either “Man” or “Woman”, which we don’t normally do in migrations. There’s nothing magical happening in migrations – we can put any rails command in there and it will get executed when the migration is run. In this case, we want to do an SQL update on all the records, and change the value in ‘type’ depending on the value of ‘male’. As it happens there’s a nice rails helper for this, called ‘update_all’. This allows us to enter the new value and an optional condition, without having to use SQL.

Looking at the ‘male’ field, we see that it happens to default to true (Come and see the sexism inherent in the system! Joking aside, if you’re going to default a boolean field, always default it to 0 (false) so that NULL and false are sort of equivalent.  Anyway lets assume that whoever set this table up hadn’t had their morning coffee yet). That means that while a woman has ‘male = false’, a man might have ‘male = NULL’. With our calls to update_all, we should follow this logic, and first set all of the values of ‘type’ to ‘Man’, then set it to ‘Woman’ where ‘male = false’. So the calls to update_all will be –

Person.update_all(‘type = “Man”‘)

(note the quotes here – single around the whole argument, double around the string “Man”)

Person.update_all(‘type = “Woman”‘, ‘male = 0’)

Note that we compare male against ‘0’, not ‘false’. This is because the database (well mysql at least) stores bools as 0 and 1 values in a ‘tinyint’ field.

So, here’s the whole migration:

class AddTypeToPeople < ActiveRecord::Migration
def self.up
add_column :people, :type, :string
Person.update_all(‘type = “Man”‘)
Person.update_all(‘type = “Woman”‘, ‘male = 0’)
end

def self.down
remove_column :people, :type
end
end

Then run it with (at the command line)
rake db:migrate

That should be it for stage 1. We can check by looking in the database, either using a gui (such as heidisql) or an sql request like

select male, type from dbname.people

which will show us only the male and type columns, which we can look down to see that the values match up properly.

Stage 2 – This is easier – first we need to add two new models, Man and Woman, which extend Person. Super simple.

in app/models/man.rb

class Man < Person
end

in app/models/woman.rb

class Woman < Person
end

That’s it! We can now call methods on Man and Woman as we would on Person.

Actually, we’re not quite out of the woods yet – what happens when a new person is added to the database, or when someone whose gender was left blank is updated to be female? (ie male = false) We need to do a callback, that calls a method to correctly set the value of ‘type’ whenever a record is updated or created. For our purposes, before_save is fine.

So, in person.rb –

class Person < ActiveRecord::Base

#will be triggered when saving a new or updated object
before_save :set_type

#and now the method that the callback calls. Remember that male
#defaults (bizarrely) to ‘true’

def set_type
if self.male == false
self.type = “Woman”
else
self.type = “Man”
end
end

#or, for those who like conditional expressions –
def set_type
self.type = (self.male == false ? “Woman” : “Man”)
end



end

This should keep our database in line.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: