ActiveRecord's confusing Single Table Inheritance behavior with NULL type

Published:
Recently I spent hours debugging an issue in a Rails project where ActiveRecord was causing MySQL errors trying to create a User object of a class at the top level of a Single Table Inheritance model structure.  It turns out `.create` behaves differently based on the default value of the type field!
I recently had the pleasure of participating in porting a very large and complex Rails project from DataMapper to ActiveRecord. Among the models that had to be ported was a set of User models that used Single Table Inheritance (STI) to distinguish one type from another. Here is a simplified version of what that looks like:
 
class User < ActiveRecord::Base
                        ...
                      end
                      
                      class AdminUser < User
                        ...
                      end

Open in new window

In this case, the same table is used to store data about both Users and AdminUsers. Generally, the 'type' field is used to tell the difference between Users and AdminUsers, by storing the class name of the object being saved (i.e. 'AdminUser'). This all works out fine 99% of the time. When you create a new model that uses STI, ActiveRecord presumably takes care of setting up a default value for the column and marking it as non nullable, etc.

I say presumably, because I haven't actually done it - my database was already setup by DataMapper years prior. And this is what happens when you try to create a Single Table Inheritance model (User.create) on a table that doesn't have a default value for the 'type' column:
 
ActiveRecord::StatementInvalid: Mysql::Error: Column 'type' cannot be null

Open in new window

ActiveRecord tries to insert a new row, setting the type column to NULL, which is a horrible way to store data for an STI model! Fortunately, in my case, type is a non-nullable column, so I get an error.

My real confusion came when I updated my dev database to have a default for the type column (because somehow the production database already had a default, so clearly my dev database was just out of sync):
 
mysql> ALTER TABLE users MODIFY COLUMN type varchar(50) NOT NULL DEFAULT 'User';
                      ...[ok]
                      mysql> exit
                      Bye
                      $> bundle exec rake db:test:prepare # <-- My Mistake
                      ...[ok]

Open in new window

I thought this was all I had to do, but it turns out running db:test:prepare just matches your test database to your schema.rb file, and my schema.rb file hadn't been updated, so now User.create is working fine in development, but broken in testing, and THAT kind of failure is the most fun you can possibly have trying to debug a test: the kind that only happens in the test environment! :D

Eventually, I came to understand all of the above, in addition to the fact that I needed to run db:migrate in order to update my schema.rb file BEFORE running db:test:prepare. Once I did that: voila! User.create actually uses the default value of the type column to insert new User objects.

Moral of the story:
  • NEVER let your development database get out of sync with production. If it is: blow it away with a db:schema:load and start over with new dev data! (Or get a production dump, or something)
  • Choose your ORM wisely. R.I.P. DataMapper - I'll miss your elegant abstractions... but not your bugs.
0
837 Views

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.