16 Jan

Legacy Rails - beware of 'type' columns

Earlier this week I was writing an app that had models connected to a number of legacy databases. To do this, I followed PragDave’s example of subclassing from ‘gatekeeper’ models that establish separate database connections. Without going into too much detail, here’s an example:

class Finance < ActiveRecord::Base
  self.abstract_class = true
  establish_connection(
    ActiveRecord::Base.configurations["finance_#{ENV['RAILS_ENV']}"]
  )
end

class Receipt < Finance
end

class Income< Finance
end

Looks good, but I hit a snag – one of my legacy tables had a ‘type’ column defined. When Rails sees a column named ‘type’ for models that aren’t immediate children of ActiveRecord::Base, it assumes that column holds the class name associated with your model. This is how Rails implements single table inheritance, and if that’s not what you intended, you’ve got a little extra work ahead of you.

Earlier this week I was writing an app that had models connected to a number of legacy databases. To do this, I followed PragDave’s example of subclassing from ‘gatekeeper’ models that establish separate database connections. This minimizes the number of active database connections, and looks good too. Without going into too much detail, here’s an example:

class Finance < ActiveRecord::Base
  self.abstract_class = true
  establish_connection(
    ActiveRecord::Base.configurations["finance_#{ENV['RAILS_ENV']}"]
  )
end

class Receipt < Finance
end

class Income< Finance
end

Looks good, but I hit a snag – one of my legacy tables had a ‘type’ column defined. When Rails sees a column named ‘type’ for models that aren’t immediate children of ActiveRecord::Base, it assumes that column holds the class name associated with your model. This is how Rails implements single table inheritance, and if that’s not what you intended, you’ve got a little extra work ahead of you.

When you attempt to do a database operation on that model, your SQL queries will contain an additional clause on your type column. Here’s the output from my development log when I try to invoke Receipt.count:

SQL (0.000) SELECT count(*) as count_all FROM receipts where ( ( receipts.`type` = 'Receipt ) )

I think this implementation of inheritance is kind of cool, but unfortunately for me, my ‘type’ column is strictly for historical purposes. Maybe it holds an acronym, a single letter, who knows. Maybe there’s dozens of types, and I don’t want to write inherited class models for all of them.

Okay, so the solution? Disable single table inheritance by overriding ActiveRecord’s inheritance_column method. Let’s return to our Receipt class:

class Receipt < Finance
  def self.inheritance_column
    nil
  end
end

Okay, that will fix up our queries, but we’re not done yet – the type column is just full of surprises. It turns out that type is a Ruby core method, otherwise known as Object#type. Allow me to demonstrate:

$ ruby script/console
Loading development environment.
>> Receipt.find(:first).type
=> Receipt

Great, all that work, and we still can’t use our type column. Our best plan of action from here is to wrap it with some read/write methods, using a different name like ‘category’ instead. It’s a bit of a cop out, I know, but right now I’m out of ideas until it the method becomes fully deprecated in the next version of Ruby.

class Receipt < Finance
  ...
  
  def category
    attr_reader :type
  end
  
  def category=(type)
    attr_writer :type, type
  end
end

Success! … and that’s why you should avoid using ‘type’ columns for anything but inheritance lookups.