To try and ease my pain, I am refactoring a Rails 2.3.5 application using Hobo from multiple tables to a single table. I’m talking about models which are essentially the same: companies, people and employees. I went from this:

 1 class Company < ActiveRecord::Base
 2   has_many :employees
 3   has_many :people, :through => :employees
 4 end
 5 
 6 class Person < ActiveRecord::Base
 7   has_many :employees
 8   has_many :companies, :through => :employees
 9 end
10 
11 class Employee < ActiveRecord::Base
12   belongs_to :company
13   belongs_to :person
14 end

To an STI-enabled class hierarchy:

 1 class Addressee < ActiveRecord::Base
 2   self.abstract_class = true
 3 end
 4 
 5 class Company < Addressee
 6   has_many :employees
 7   has_many :people, :through => :employees
 8 end
 9 
10 class Person < Addressee
11   has_many :employees
12   has_many :companies, :through => :employees
13 end
14 
15 class Employee < Addressee
16   belongs_to :company
17   belongs_to :person
18 end

Can you spot the mistake? Here’s where it breaks down:

1 $ script/console
2 > PersonAddressee.count
3 SQL (9.9ms)   SELECT count(*) AS count_all FROM "addressees"
4 => 10231

When the classes are defined with abstract_class? set to true, calling Company#count executes the following:

  1. ActiveRecord Company#superclass, which answers Addressee
  2. ActiveRecord calls Addressee#abstract_class?, which answers true
  3. Because Company#base_class == Company#superclass, no STI is involved, and ActiveRecord queries the addressees table with no STI hints (type column).

When removing the abstract class declaration, here’s how it works:

  1. ActiveRecord Company#superclass, which answers Addressee
  2. ActiveRecord calls Addressee#abstract_class?, which answers false
  3. ActiveRecord Addressee#superclass, which answers ActiveRecord::Base
  4. Because Company#base_class != Company#superclass, STI is involved and ActiveRecord adds the type hint to the addressees table query.

The documentation is quite clear though:

abstract_class?()

Returns whether this class is a base AR class. If A is a base class and B descends from A, then B.base_class will return B.

The documentation on #base_class is much better though:

base_class()

Returns the base AR subclass that this class descends from. If A extends AR::Base, A.base_class will return A. If B descends from A through some arbitrarily deep hierarchy, B.base_class will return A.

And current HEAD documentation is the best of them all:

base_class()

Returns the base AR subclass that this class descends from. If A extends AR::Base, A.base_class will return A. If B descends from A through some arbitrarily deep hierarchy, B.base_class will return A.

If B < A and C < B and if A is an abstract_class then both B.base_class and C.base_class would return B as the answer since A is an abstract_class.

Luckily, I found this before users got a chance to report bugs. And no, I would never have thought to add a test for this: I’m not in the habit of testing base ActiveRecord functionality.

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on