Robot Has No Heart

Xavier Shay blogs here

A robot that does not have a heart

Class Table Inheritance and Eager Loading

Consider a typical class table inheritance table structure with items as the base class and dvds and cars as two subclasses. In addition to what is strictly required, items also has an item_type parameter. This denormalization is usually a good idea, I will save the justification for another post so please take it for granted for now.

The easiest way to map this relationship with Rails and ActiveRecord is to use composition, rather than trying to hook into the class loading code. Something akin to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Item < ActiveRecord::Base
  SUBCLASSES = [:dvd, :car]
  SUBCLASSES.each do |class_name|
    has_one class_name
  end

  def description
    send(item_type).description
  end
end

class Dvd < ActiveRecord::Base
  belongs_to :item

  validates_presence_of :title, :running_time
  validates_numericality_of :running_time

  def description
    title
  end
end

class Car < ActiveRecord::Base
  belongs_to :item

  validates_presence_of :make, :registration

  def description
    make
  end
end

A naive way to fetch all the items might look like this:

1
Item.all(:include => Item::SUBCLASSES)

This will issue one initial query, then one for each subclass. (Since Rails 2.1, eager loading is done like this rather than joining.) This is inefficient, since at the point we preload the associations we already know which subclass tables we should be querying. There is no need to query all of them. A better way is to hook into the Rails eager loading ourselves to ensure that only the tables required are loaded:

1
2
3
Item.all(opts).tap do |items|
  preload_associations(items, items.map(&:item_type).uniq)
end

Wrapping that up in a class method on items is neat because we can then use it as a kicker at the end of named scopes or associations – person.items.preloaded, for instance.

Here are some tests demonstrating this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require 'test/test_helper'

class PersonTest < ActiveRecord::TestCase
  setup do
    item = Item.create!(:item_type => 'dvd')
    dvd  = Dvd.create!(:item => item, :title => 'Food Inc.')
  end

  test 'naive eager load' do
    items = []
    assert_queries(3) { items = Item.all(:include => Item::SUBCLASSES) }
    assert_equal 1, items.size
    assert_queries(0) { items.map(&:description) }
  end

  test 'smart eager load' do
    items = []
    assert_queries(2) { items = Item.preloaded }
    assert_equal 1, items.size
    assert_queries(0) { items.map(&:description) }
  end
end

# Monkey patch stolen from activerecord/test/cases/helper.rb
ActiveRecord::Base.connection.class.class_eval do
  IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]

  def execute_with_query_record(sql, name = nil, &block)
    $queries_executed ||= []
    $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
    execute_without_query_record(sql, name, &block)
  end

  alias_method_chain :execute, :query_record
end

I talk about this sort of thing in my “Your Database Is Your Friend” training sessions. They are happening throughout the US and UK in the coming months. One is likely coming to a city near you. Head on over to www.dbisyourfriend.com for more information and free screencasts

  1. CodeOfficer says:

    I'm confused. Why are you not using ActiveRecord's built in support for Single Table Inheritance?

  2. FirstTimer says:

    @CodeOfficer There are various ways to map a class hierarchy. A table for the class hierarchy (known as STI), a table per concrete (none - abstract) class and a table per class.
    Of course you can use STI here (would actualy make sense since all attributes are shared) - but this blog entry explores the other case - as the title ob the blog entry says.

  3. Xavier Shay says:

    @CodeOfficer: STI isn't always a good option, I've written about it before

    As FirstTimer points out, in this trivial case STI would make sense, but I've deliberately used a simple example in order to demonstrate the technique.

  4. Ganesh Shankar says:

    Hey Xavier,

    Thanks for the example! I'm actually trying to implement CTI in a Rails 3 application at the moment and am a bit confused by something.

    Using your example to illustrate my situation... I want to have common attributes stored in Item (e.g. name, price) and Car and Dvd would have their own specific attributes (e.g. registration for Car).

    If I'm updating the attributes of a Car, how do I easily set the common attributes in the parent table? One way is to hook into a after_save callback, is this the best way to approach this?

  5. Xavier Shay says:

    Ganesh, I'll put together an example for you, I reckon accepts_nested_attributes will be the best solution in your case.

  6. Mark Wilden says:

    Good article. However, I like it best when realistic code is used. If an article is explaining a good way to use Class Table Inheritance, why not use a good example of Class Table Inheritance? Would it really make things that much more complicated?

  7. Xavier Shay says:

    I added some extra attributes to the subclasses to make them different. Also! I said above STI would probably make sense for this example, that's a bit of a cop out. Yes, Car and Dvd both have a non-null string column, but they are still different attributes so don't belong in the same column.

  8. Alan says:

    I'd be like FUUUUUU if someone did that SUBCLASSES constant in my code. Have you considered real class inheritance and class.inherited on the base class? I'd probably define the action in a module so I could reuse it too.

  9. Xavier Shay says:

    Alan, fair point on the SUBCLASSES constant. Composition was the simplest method to show off this eager loading technique, it doesn't need any extra 'magic' to deal with class loading.

  10. Data Recovery says:

    This is awesome information on inheritance with good examples. Now I can easily practice this with the help of giving example. I thing I have noticed that you have great knowledge on sql commands even deep knowledge which shows your all basic concepts are clear.

    Regards
    Malcom
    File Recovery Software
    http://www.datadoctor.biz

Post a comment


(lesstile enabled - surround code blocks with ---)

A pretty flower Another pretty flower