a compromise

Jared Carroll

On a sidebar in the latest edition of Agile Web Development I noticed something. It was a description of a class query method in ActiveRecord::Base called abstract_class?. Just what I was looking for.

Previously, I complained about not being able to do real behavior-based inheritance (the way inheritance should be used) in Rails. To me Student and Teacher are not subclasses of Person because they both have a name. State means nothing, it should be based on behavior.

Say in our app, we have users and companies. Both of which can have any number of addresses. In other words, they’re both addressable.

Let’s model it out:

class Addressable < ActiveRecord::Base
  has_many :addresses
end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base
  belongs_to :addressable
end

Now, using STI we’d need at an absolute minimum the following schema:

addressables (id, type)
addresses (id, addressable_id)

That is, our User and Company objects would both be stored in the addressables table.

Let’s say that the state about users and companies in our app is very different, such that storing the union of all their attributes in 1 table is ugly, inefficient and will result in a lot of nulls in each row. Instead I want separate tables for my users and companies. However, you can’t do that in Rails when it comes to inheritance.

Now apparently, if this ActiveRecord::Base class query method named #abstract_class? returns true Rails will never try to find a corresponding table for it in the database. That means Rails will assume its subclasses have their own tables.

Let’s rewrite the above example:

class Addressable < ActiveRecord::Base
  has_many :addresses

  def self.abstract_class?
    true
  end
end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base
  belongs_to :addressable
end

Sweet.

No wait.

That doesn’t work. Address belongs_to addressable, when we say address.addressable Rails will go looking for an addressables table and fail.

So we’re going to have to bust out polymorphic associations.

Rewrite:

class Addressable < ActiveRecord::Base
  has_many :addresses, :as => :addressable

  def self.abstract_class?
    true
  end
end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

The schema:

 users (id, etc...)
 companies (id, etc...)
 addresses (id, addressable_id, addressable_type, etc...)

There we go.

Nice behavior-based inheritance. We get to refer to users and companies as addressables and we get to say:

has_many :addresses, :as => :addressable

all in 1 place.

The alternative, and common Rails idiom when using polymorphic associations, would not include the Addressable class, and be something along the lines of:

class User < ActiveRecord::Base
  acts_as_addressable
end

class Company < ActiveRecord::Base
  acts_as_addressable
end

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

module ActsAsAddressable
  def acts_as_addressable
    self.class_eval do
      has_many :addresses, :as => :addressable
    end
  end
end

ActiveRecord::Base.extend ActsAsAddressable

With that ActsAsAddressable module defined as a plugin in ‘vendor/plugins’. I don’t like this style because of the need to put the acts_as_addressable in each model.

And then you say, But that’s more explicit, you look at the model, see the acts_as_addressable declaration, and know right away its addressable.

Yes that’s true but you can get the same effect in the previous solution because the model subclasses Addressable. There’s no acts_as_addressable declaration but by subclassing Addressable you can infer the same information.

I don’t care much for the whole ‘acts_as’ naming convention either I’d rather just use plain Ruby ‘include’ and rewrite the above as:

class User < ActiveRecord::Base
  include Addressable
end

class Company < ActiveRecord::Base
  include Addressable
end

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

module Addressable
  def self.included(clazz)
    clazz.class_eval do
      has_many :addresses, :as => :addressable
    end
  end
end

And just put the Addressable module in ‘lib/addressable.rb’, no need for a plugin. The minute your users and companies become something more than just addressable, such as taggable, you’ll have to use either the acts_as plugin style or just ‘include’ because Ruby only has single inheritance. Then the whole beauty of ActiveRecord::Base#abstract_class? is lost anyway.

However, ActiveRecord::Base#abstract_class? does finally give Rails developers the ‘Concrete table inheritance’ OR mapping pattern for inheritance relationships.