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.