On the chopping Bloc

June 14, 2012 at 7:25pm
Home

Going deeper - Part I

It’s funny how seemingly simple features can trigger huge changes to an app’s data structure.

Case in point: my Dealbook app currently has three main models to store information on deals - Company, Investor and Deal. The Investor model represents the entity making the investment (buyer). The Company model represents the entity receiving the investment (seller). Deal is a through model linking Companies and Investors, but also containing deal information, such as close date.

The associations look like this:

class Company < ActiveRecord::Base
   has_many :deals
   has_many :investors, :through => :deals
end

class Investor < ActiveRecord::Base
   has_many_and_belongs_to_many :deals
   has_many :companies, :through => :deals
end

class Deal < ActiveRecord::Base
   belongs_to :company
   has_many_and_belongs_to_many :investors
end

Notice that a given Deal only belongs to one company, but has many investors. That makes sense, because a typical deal usually involves several investors putting money into a single company.

So far, so good. My app was running happily with these model associations, until I ran into the following scenario: what if one of the investors is actually another company?

With the current models, I would have to keep two separate records for the same company (both as Company and as Investor). This is obviously a bad idea, since the company’s data would be scattered around. Ideally, the Company object should be able to store investments into other companies directly.

It seems inevitable that we will need companies to reference themselves. Since dealing with self-references can be mind-numbing, I will skip it for now and address other issues first. To avoid the self-reference, I will use a workaround by temporarily creating a Corporate model to represent a company when it is the buyer. The current Company model will represent only the target company for now. Later, we will try to merge these two models into one.
 

Polymorphism

The first step to solving the puzzle is finding a way to allow Deal buyers to be both Investors or Corporates. This most likely involves creating a polymorphic association between deals and buyers:

class Company < ActiveRecord::Base
  has_many :deals
end

class Deal < ActiveRecord::Base
  belongs_to :company
  belongs_to :buyer, :polymorphic => true
end

class Investor < ActiveRecord::Base
  has_many :deals, :as => :buyer
  has_many :companies, :through => :deals
end

class Corporate < ActiveRecord::Base
  has_many :deals, :as => :buyer
  has_many :companies, :through => :deals
end

There is one problem here, however. (Can you spot it?) The current association between Deals and Investors is not has_many, but rather has_and_belongs_to_many (HABTM), which does not accept polymorphism. Changing it to a simple has_many relationship is not an option, since an investor can have many deals and a deal can have many investors.
 

Two levels deep

One solution would be to change the HABTM association into a has_many through, which does allow polymorphism. Since we already have a ‘has_many through deals’ association, we would now have two intermediary models between target companies (Company) and their buyers (Investors or Corporates). Fortunately, multi-level through associations are supported as of Rails 3.1.

Here, I included a new through model called Dealing:

class Company < ActiveRecord::Base
  has_many :deals
  has_many :participations, :through => :deals
end

class Deal < ActiveRecord::Base
  belongs_to :company
  has_many :dealings
end

class Dealing < ActiveRecord::Base
  belongs_to :deal
  belongs_to :buyer, :polymorphic => true
end

class Investor < ActiveRecord::Base
  has_many :dealings, :as => :buyer
  has_many :deals, :through => :dealings
  has_many :companies, :through => :deals
end

class Corporate < ActiveRecord::Base
  has_many :dealings, :as => :buyer
  has_many :deals, :through => :dealings
  has_many :companies, :through => :deals
end

This works! Now we can access companies directly from an investor by simply using:

> an_investor.companies
# returns array of companies invested by an_investor

The only catch is that polymorphic through associations only go in one direction, so trying the opposite call does not work:

> a_company.buyers
> a_company.investors
> a_company.corporates
# all of the above throw NoMethodError

There are a few ways around this, which I will cover later. (If you’re curious, check out this post.)

So now that we can accept both types of buyers in Deals, it’s time to eliminate the Corporate model and create a self-referencial Company.

 
TO BE CONTINUED…