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.