Rails Made My Day

Rails made my day yesterday when I delved into the truly awesome power of declarative associations.

I’ll use a pretty common scenario as an example, which, in the old days, would have taken me weeks to untangle:

We have three models, Person, Company and Address.

There are a number of key relationships that need to be modelled (ahem) and these are summarized as follows:

  • a person can have zero or more addresses
  • a company can have one or more addresses
  • an address can belong to one or more people and/or companies.
  • a person should have at most one address considered as the ‘main’ address
  • a company must have exactly one address considered as the ‘main’ address

There are also some basic UI requirements which need to be provided:

  • When creating or updating a person, the user should be able to edit the main address in the same form, or leave it blank
  • When creating or updating a company, the user should be able to edit the main address in the same form

The first observation I would make is that, although an address can, technically, belong to multiple models, it is counter-productive to try to model this in the schema.  Instead, we should treat an address as a disposable collection of text attributes, representing a physical address, which may happen to be duplicated multiple times in our schema (I feel better already).

I tend to think in third normal form (nod to the relational database die-hards.  It’s an old habit and Rails is beginning to cure me…), so my initial thoughts were as follows:

Person Company Address Model UML

You might choose a slightly different schema, with addressType in Address, but for me the addressType is contextual to the Person or Company, not the address itself.  Minor semantics.

This would map into rails as follows:

 class Person < ActiveRecord::Base
   has_many :person_addresses
   has_many :addresses, :through => :person_addresses
 end
 
 class PersonAddress < ActiveRecord::Base
   belongs_to :person
   belongs_to :address
 end
 
 class Company < ActiveRecord::Base
   has_many :company_addresses
   has_many :addresses, :through => :company_addresses
 end
 
 class CompanyAddress < ActiveRecord::Base
   belongs_to :company
   belongs_to :address
 end
 
 class Address < ActiveRecord::Base
 end
 

I hope you’re still with me, because this is where it gets exciting(!)

The next job is to build the logic that manages creation of the main address (and the link object, e.g. PersonAddress) for new records, with an address_type of ‘Main’.  Then that all needs to be validated.  After we’ve muddled through and tested it all, we need to think about the UI logic and how to submit a main address to the controller using the same form as the person or company.  There there’s updates and deletes to get stuck into.

I feel nauseous.   Not least, because I’m going to have to do this all over again with telephone numbers…

BUT WAIT!

After much more reading into associations in Rails, I decided to test the mettle.  Could Rails actually provide a declarative solution to this whole thorny problem?

Oh boy.  And then some.

Please see the models below to whet your appetite.  When I found this worked, it blew me away.

class Address < ActiveRecord::Base
  attr_accessible ...etc, :address_type

  belongs_to :addressable, :polymorphic => true
end

class Person < ActiveRecord::Base
   attr_accessible ...etc, :main_address_attributes

   has_many :addresses, :as => :addressable, :dependent => :destroy

   has_one :main_address, :class_name => "Address",
     :as => :addressable,
     :conditions => { :address_type => "Main"}

   accepts_nested_attributes_for :main_address,
     :allow_destroy => :true,
     :reject_if => proc {|attrs|
       attrs[:address_line_1].empty? || attrs[:postcode].empty?
     }
 end

class Company < ActiveRecord::Base
   attr_accessible ...etc, :main_address_attributes

   has_many :addresses, :as => :addressable, :dependent => :destroy

   has_one :main_address, :class_name => "Address",
     :as => :addressable,
     :conditions => { :address_type => "Main"}

   accepts_nested_attributes_for :main_address,
     :allow_destroy => :true
 end

I won’t go into the details of poymorphic associations (or how to create the migrations and multi-model views to enable the main address to be entered alongside the owning model); it’s already well documented on Rails Guides and Getting Started with Rails.

However, some explanation is necessary:

  • The :polymorphic option allows the Address class to be associated to any other class via a proxy called :addressable, without having to know which other classes it is associated to (good encapsulation). Rails handles this with some additional columns on the address table.
  • Person and Company each have an additional association via a proxy, called :main_address in each case. The :conditions not only declare constraints on this association, but also act as build logic (see below)
  • accepts_nested_attributes_for tells Rails that the main address attributes can be passed to the Person model along with the person attributes (this enables :main_address to be built automatically from a form containing fields for both Person and Address models). Note the addition of :main_address_attributes in attr_accessible

These declarations are all that is required to do all of the work!

Now we can write statements like this:

@person.main_address || @person.build_main_address
@person.main_address.address_line_1 = '742 Evergreen Terrace'
# etc...

which builds the main address (already populated with the attribute address_type = "Main") and associates it with the person model, if the main address does not already exist. The main address also appears in the @person.addresses collection.

It all works perfectly in the UI also and the :reject_if option ensures that the address doesn’t get created if the address attributes sent along with the person attributes do not have an address_line_1 and postcode. Notice there is no :reject_if option on the Company model because the main address is not optional, according to our specification.

Beautiful.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s