Topic: Polymorphic Phones/Addresses and Users/Organizations

Rails is allowing me to try things that would have been just too difficult or time-consuming with other platforms. For example, most of my apps have a model for a User. The way I used to do it, the User would have attributes for one or two addresses ("mailing" and "shipping") and two or three phone numbers ("home", "work", "cell"). Same with an Organization or Company. But I was stuck with 1, 2, or 3 addresses and phones because it was just too hard to model 1..n phones and addresses.

Now I'd like to model the real world a little more closely and create models for User and Organization with separate models for Phone and Address, allowing unlimited phones and addresses for each.

I really like Diego Scataglini's contribution to the O'Reilly "Rails Cookbook", recipe 3.21, where he describes "Factoring Out Common Relationships with Polymorphic Associations," and receipe 3.22, where he describes "Mixing Join Models and Polymorphism for Flexible Data Modeling." The code is a bit complex; it looks like Evan Weaver

Re: Polymorphic Phones/Addresses and Users/Organizations

has_many_polymorphs is good when you need a many-to-many association between the two. Here I don't think you do. That is, does an address really need to belong to many people/organizations? It is proably only owned by one, correct? In that case the code is significantly simpler.

class Person < ActiveRecord::Base
  has_many :phones, :as => :phonable
  has_many :addresses, :as => :addressable
end

class Organization < ActiveRecord::Base
  has_many :phones, :as => :phonable
  has_many :addresses, :as => :addressable
end

class Phone < ActiveRecord::Base
  belongs_to :phonable, :polymorphic => true
end

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


Then you just have to create those four tables - no other tables are necessary. You will need to add a "*_type" column for addressable/phonable in the appropriate tables though. I can go into more detail if you need.

Railscasts - Free Ruby on Rails Screencasts

Re: Polymorphic Phones/Addresses and Users/Organizations

Thanks for the clarity, Ryan. You're right, an address or phone usually belongs to one person or organization. I suppose there are cases where a person and organization both have the same phone number (a sole proprietor, for example) but I don't think there's any drawback to having that phone number duplicated in the phones table as if it were two separate numbers, ignoring the fact that one phone number might belong to both a User and an Organization. (This works as long as the database isn't set up to index the phone number as unique.)

There are also common cases where many Users have the same phone number. For example, five users all have the same work phone number. This is even more likely with work addresses, where potentially dozens of users have the same work address. I think these qualify as many-to-many polymorphic relationships? But maybe it's not worth the trouble to implement.

Mostly I want to have a Phone or Address controller and search for everyone with phone number "x" or see everyone at address "y". I can do that just fine with the one-to-many polymorphic association and it means much simpler code, as you say.

I've added a phonable_type and addressable_type to the phones and addresses tables, respectfully. Also needed phonable_id and addressable_id columns as well.

Here's the models and the migration.

class User < ActiveRecord::Base
  has_many :phones, :as => :phonable, :dependent => :destroy
  has_many :addresses, :as => :addressable, :dependent => :destroy
  def full_name
    "#{first_name} #{last_name}"
  end
end

class Organization < ActiveRecord::Base
  has_many :phones, :as => :phonable, :dependent => :destroy
  has_many :addresses, :as => :addressable, :dependent => :destroy
  def full_name
    name
  end 
end

class Phone < ActiveRecord::Base
  belongs_to :phonable, :polymorphic => true
  has_one :user, :as => :phonable
  has_one :organization, :as => :phonable
end

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
  has_one :user, :as => :addressable
  has_one :organization, :as => :addressable
  def address
    "#{thoroughfare}, #{locality}, #{admin_area}, #{country}"
  end
end

class CreatePhonesAndAddressesTables < ActiveRecord::Migration
  def self.up
    create_table :phones do |t|
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
      t.column :phonable_id, :integer, :null => false
      t.column :phonable_type, :string, :null => false
      t.column :tag_for_phone, :string # could be "work phone", "home phone", etc
      t.column :number, :string # redundant but makes searching easier
      t.column :country_code, :string
      t.column :locality_code, :string # in the US, "area code", elsewhere "city code"
      t.column :local_number, :string
    end

    create_table :addresses do |t|
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
      t.column :addressable_id, :integer, :null => false
      t.column :addressable_type, :string, :null => false
      t.column :tag_for_address, :string # could be "mailing address", "shipping address", etc
      t.column :country, :string
      t.column :admin_area, :string # in the US, "state"
      t.column :locality, :string # in the US, "city"
      t.column :thoroughfare, :string # in the US, "address"
      t.column :postal_code, :string
    end

    add_index :phones, :number # don't make this unique!
    add_index :addresses, :thoroughfare # don't make this unique!

  end

  def self.down
    drop_table :phones
    drop_table :addresses
  end
end


I had no trouble displaying the phone numbers and the addresses for the User and an Organization. I used code in my User and Organization "show" views like this (just showing app/views/users/show, app/views/organizations/show is similar):

<% unless @user.phones.empty? %>    
    <label>Phone Numbers</label>
        <% for phone in @user.phones %>
          <%= link_to phone.number, phone_path(phone) %><br />
        <% end %>
<% end -%>

<br />
    <%= link_to 'Add Phone Number', new_user_phone_path(@user) %>
<br />

<% unless @user.addresses.empty? %>   
    <label>Addresses</label>
        <% for address in @user.addresses %>
          <%= link_to address.full_address, address_path(address) %><br />
        <% end %>
<% end -%>

<br />
    <%= link_to 'Add Address', new_user_address_path(@user) %>
<br />


I ran into a challenge when I wanted to display the Phone or Address and show the User name or Organizaton name. To get at the data from the Phone or Address side of the relationship, I had to add a "has_one" relationship to the Phone and Address models like so:

class Phone < ActiveRecord::Base
  belongs_to :phonable, :polymorphic => true
  has_one :user, :as => :phonable
  has_one :organization, :as => :phonable
end

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
  has_one :user, :as => :addressable
  has_one :organization, :as => :addressable
end

My view code in app/views/phones/show looks like this:

<% if @phone.phonable_type == 'User' -%>    
    <label>Phone for</label>
    <span>
        <%= link_to @phone.phonable.full_name, user_path(@phone.phonable) -%><br />
    </span>
<% end -%>
<br />
<% if @phone.phonable_type == 'Organization' -%>   
    <label>Phone for</label>
    <span>
        <%= link_to @phone.phonable.full_name, organization_path(@phone.phonable) -%><br />
    </span>
<% end -%>

I added a "full_name" method to both the User and Organization models to accomodate the difference between the "first_name" and "last_name" methods of the User and the "name" method of the Organization.

I'd like to eliminate the "if statements" but I don't know a way around that since the clickable path to the user or organization must differ (any ideas?).

I also don't like using "@phone.phonable" because it's not obvious. It took a bit of research to find that "phonable" was the only way to get "user". It would make more sense if "@phone.user" or "@phone.organization" returned what we need. Is there any easy way to do that?

I wonder if I've done the right thing with the "new" view and controller "create" action. It looks funky to me because I have to set a hidden field in the "new" view to pass the user_id or organization_id into the controller "create" action.

Here's the app/controllers/phones_controller "new" method:

def new
  @user = User.find(params[:user_id]) if params[:user_id]
  @organization = Organization.find(params[:organization_id]) if params[:organization_id]
  @phone = Phone.new
end

Here's the app/views/phones/new view:

<h2>New Phone Number</h2>
<label>For</label>
    <span>
        <%=h @user.full_name if @user -%>
        <%=h @organization.full_name if @organization -%>
    </span><br />
<% form_for :phone, :url => phones_path do |form| -%>
    <%= (hidden_field :user, :id, :value => @user.id) if @user -%>
    <%= (hidden_field :organization, :id,  :value => @organization.id) if @organization -%>
    <%= render :partial => 'form', :object => form -%>
    <p><%= submit_tag 'Create' -%></p>
<% end -%>

And here's the app/controllers/phones_controller "create" method:
def create
  @user = User.find(params[:user][:id]) if params[:user]
  @organization = Organization.find(params[:organization][:id]) if params[:organization]
  @phone = Phone.new(params[:phone])
  @phone.number = "+#{@phone.country_code} #{@phone.locality_code} #{@phone.local_number}"
  respond_to do |format|
    if @phone.save
      @user.phones << @phone if @user
      @organization.phones << @phone if @organization
      @user.save if @user
      @organization.save if @organization
      flash[:notice] = "Successfully added #{@phone.number}."
      format.html { redirect_to phone_url(@phone) }
      format.xml  { head :created, :location => phone_url(@phone) }
    else
      flash[:notice] = "Unable to add #{@phone.number}."
      format.html { render :action => "new" }
      format.xml  { render :xml => @phone.errors.to_xml }
    end
  end
end

This business of passing hidden ids is a bit dismal.

I also had challenges getting the routing to work for this implementation. Here's what works for me. I'm afraid RESTful routes remain mysterious to me! Maybe there's a better way to configure the routes.

map.resources :addresses
map.resources :phones
map.resources :organizations do |organization|
              organization.resources :organization_phones,
                :opaque_name => :phones,
                :controller => :phones
              organization.resources :organization_addresses,
                :opaque_name => :addresses,
                :controller => :addresses
              end
map.resources :users do |user|
               user.resources :user_phones,
                 :opaque_name => :phones,
                 :controller => :phones
               user.resources :user_addresses,
                 :opaque_name => :addresses,
                 :controller => :addresses
               end

That's a lot of code, a handful of questions, and hopefully some guidance for anyone else who needs to implement this very common Phone/Address Person/Organization functionality. I'd like to find a "best practice" example of this (anyone?) but short of that, at least here are some ideas to start with.

Re: Polymorphic Phones/Addresses and Users/Organizations

fortuity wrote:

There are also common cases where many Users have the same phone number. For example, five users all have the same work phone number. This is even more likely with work addresses, where potentially dozens of users have the same work address. I think these qualify as many-to-many polymorphic relationships? But maybe it's not worth the trouble to implement.

Often what may seem like duplication isn't. A good question to ask yourself is if the object (address, phone) is changed in one location, should it be automatically changed in the other? It depends on the app, but I think an address should be "owned" by one user/organization so they may change it at free will without effecting other users/organizations. But that may or may not be the case in your application.

fortuity wrote:

I'm not sure if those has_one associations will work here.
I ran into a challenge when I wanted to display the Phone or Address and show the User name or Organizaton name. To get at the data from the Phone or Address side of the relationship, I had to add a "has_one" relationship to the Phone and Address models like so:

I'm surprised the has_one association works. But anyway, I recommend doing it this way:

    <label>Phone for</label>
    <span>
        <%= link_to @phone.phonable.full_name, phonable_path(@phone.phonable) -%><br />
    </span>

You can make phonable_path a helper method to generate the path depending on the type of phonable:

def phonable_path(phonable)
  if phonable.kind_of? User
    user_path(phonable)
  else
    oraganization_path(phonable)
  end
end

Same goes for the addresses.


fortuity wrote:

I added a "full_name" method to both the User and Organization models to accomodate the difference between the "first_name" and "last_name" methods of the User and the "name" method of the Organization.

Good, you should move the differences into the organization/user whenever possible to avoid if statements as you said.

fortuity wrote:

I also don't like using "@phone.phonable" because it's not obvious. It took a bit of research to find that "phonable" was the only way to get "user". It would make more sense if "@phone.user" or "@phone.organization" returned what we need. Is there any easy way to do that?

The problem is you don't know what the phone belongs to and you will have to constantly be asking if it's a user or organization - one of these will return nil. This leads to a lot of duplication and if/else statements. The point of polymorphic association is so you don't have to worry about what type of model is behind the scenes. Try to make them have the same interface so you can treat them the same (duck typing) so you don't have to worry about wether the phonable is a user or organization.

If you want, rename "phonable" to "owner" or something which is more descriptive.


fortuity wrote:

I wonder if I've done the right thing with the "new" view and controller "create" action. It looks funky to me because I have to set a hidden field in the "new" view to pass the user_id or organization_id into the controller "create" action.

Try this:

def new
  if params[:user_id]
    @phone = Phone.new(:phonable_type => 'User', :phonable_id => params[:user_id])
  else
    @phone = Phone.new(:phonable_type => 'Organization', :phonable_id => params[:organization_id])
  end
end

Then pass the phonable_type and phonable_id as hidden fields.

<%= form.hidden_field :phonable_type %>
<%= form.hidden_field :phonable_id %>

Then you don't have to worry about it anymore, it should properly set the phonable automatically when you create the phone.


fortuity wrote:

I also had challenges getting the routing to work for this implementation. Here's what works for me. I'm afraid RESTful routes remain mysterious to me! Maybe there's a better way to configure the routes.

I generally don't do nested routes. I would just do this:

map.resources :phones, :addresses, :organizations, :users

Railscasts - Free Ruby on Rails Screencasts

Re: Polymorphic Phones/Addresses and Users/Organizations

Thanks for the suggestions! It's a delight to see the cleaner code.

I appreciate your help and hope this example will be useful to others.