ActiveRecord's confusing Single Table Inheritance behavior with NULL type

Published on
3,411 Points
Last Modified:
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

class AdminUser < User

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';
mysql> exit
$> bundle exec rake db:test:prepare # <-- My Mistake

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.

Featured Post

Cloud Class® Course: MCSA MCSE Windows Server 2012

This course teaches how to install and configure Windows Server 2012 R2.  It is the first step on your path to becoming a Microsoft Certified Solutions Expert (MCSE).

Join & Write a Comment

The purpose of this video is to demonstrate how to set up the WordPress backend so that each page automatically generates a Mailchimp signup form in the sidebar. This will be demonstrated using a Windows 8 PC. Tools Used are Photoshop, Awesome…
This is Part-2 of Learning to use the Power of Mailwasher Pro so if you haven't watched Part-1 yet, I urge you to do so before watching this video. Click this link to watch Part-1 (https://www.experts-exchange.com/videos/56638/Learn-to-use-the-POWER…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month