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:
- ActiveRecord
Company#superclass, which answersAddressee - ActiveRecord calls
Addressee#abstract_class?, which answerstrue - Because
Company#base_class == Company#superclass, no STI is involved, and ActiveRecord queries theaddresseestable with no STI hints (type column).
When removing the abstract class declaration, here’s how it works:
- ActiveRecord
Company#superclass, which answersAddressee - ActiveRecord calls
Addressee#abstract_class?, which answersfalse - ActiveRecord
Addressee#superclass, which answersActiveRecord::Base - Because
Company#base_class != Company#superclass, STI is involved and ActiveRecord adds the type hint to theaddresseestable 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.