Preamble
I wanted to model connections à la connections in LinkedIn or Facebook in a Rails application. This means a many-to-many association between instances of the same class. That caused me some grief trying to get it hooked up right because you can’t rely on Rails to figure everything out.The other trick in my application is that the people involved in the connections might be users who have registered to use the application, or they might be people created by a registered user, but who aren’t registered to user the application.
Concretely, and hopefully more clearly, I have “users”, who have registered, and I have people who can be involved connections. In my app the people who aren’t registered users are “patients”.
In the course of trying to get this all to work I stumbled across three approaches to this type of problem:
- Polymorphic classes
- Single Table Inheritance (STI)
- Decorator pattern
In the end, I decided to go with the Decorator pattern. But I’ll start with the one I threw out first: Polymorphic.
Polymorphic
I got pretty far with polymorphic associations, but I couldn’t figure out how I was going to get a list of all people (patients and users) connected to another person. I could either get all patients or all users from the methods that the Rails associations gave me, but not a list of all together.I realized in writing the preamble above that I probably should have realized that what I was trying to model wasn’t really a polymorphic situation. Polymorphic in the examples I saw was used to connect an object to another object from any one of a number of unrelated classes. Of course, hindsight is 20/20.
This post convinced me that trying to get a list of all people wasn’t going to come naturally from a polymorphic approach, so I stopped pursuing it.
Single Table Inheritance
I got fired up about single table inheritance (STI) as I was reading about how to make the polymorphic approach work. A good brief write up is here: http://blog.thirst.co/post/14885390861/rails-single-table-inheritance. The Railscast is here: http://railscasts.com/episodes/394-sti-and-polymorphic-associations (sorry, it’s a pro Railscast so it’s behind a paywall).Others say I shouldn’t do STI. People say it can cause problems. One problem is if the type of an object will change, and change because of user input, it’s hard to handle. The view and controller are fixed to a certain object, so you can’t change the object type based on user input.
So here’s the code. First, create the models:
rails g model person name:string type:string provider:string uid:string
rails g model link person_a:references person_b:references b_is:string
class Person < ActiveRecord::Base
  has_many :links, foreign_key: "person_a_id"
  has_many :people, :through => :links, :source => :person_b
  scope :patients, -> { where(type: "Patient") }
  scope :users, -> { where(type: "User") }
end
class User < Person
end
class Patient < Person
end
class Link < ActiveRecord::Base
  belongs_to :person_a, class_name: "Person"
  belongs_to :person_b, class_name: "Person"
end
has_many :links,... in person.rb work was the , class_name: "Person" on the association in link.rb. With the above, I can do things like:
person = Person.find(1).first
person.people.first.name
person.people.patients.first.name
person.people.users.first.name
Decorator
In the end, perhaps the simplest was the best. I just decorated a person with an instance of a user when the person is a registered user. (This allows multiple people for a user, which might be useful for consolidating duplicate people.)Here’s what I did:
Generate the models:
rails g model link person_a:references person_b:references b_is:string
rails g model person user:references name:string
rails g model user uid:string name:string provider:string
require 'person_helper'
class Person < ActiveRecord::Base
  belongs_to :user
  has_many :links, foreign_key: :person_a_id
  has_many :people, through: :links, source: :person_b
  include PersonHelper
end
has_one instead of belongs_to, but that would put the foreign key in the wrong model.user.rb
require 'person_helper'
class User < ActiveRecord::Base
  has_many :identities, class_name: "Person"
  has_many :links, through: :identities
  has_many :people, through: :links, :source => :person_b
  include PersonHelper
end
module PersonHelper
  def users
    people.select { |person| ! person.user_id.nil? }
  end
  def patients
    people.select { |person| person.user_id.nil? }
  end
end
class Link < ActiveRecord::Base
  belongs_to :person_a, class_name: "Person"
  belongs_to :person_b, class_name: "Person"
end
person = Person.find(1).first
person.people.first.name
person.patients.first.name
person.users.first.name
user = User.find(2).first
user.users.first.name
Postscript
Another thing people don’t seem to like about STI is that it’s easy to end up with a big table full of all sorts of columns used only in a few places. Most modern database management systems aren’t going to waste a significant amount of space for unused columns, so I’m not sure what the problem is.However, it got me thinking if there isn’t a way in Rails to have more than one table under a model. Or more to the point, could you have a table for the base model class, and a different table for each of the subclasses, and have Rails manage all the saving a retrieving.
I’m sure I’m not the first person to think of this. But I’m not going to go looking for it right now.
Other Resources
Rails 4 guides on associations: http://guides.rubyonrails.org/association_basics.html and migrations: http://guides.rubyonrails.org/migrations.html.Ryan Bates’ Railscast on self-referential associations: http://railscasts.com/episodes/163-self-referential-association, and on polymorphic associations: http://railscasts.com/episodes/154-polymorphic-association.
 
 
No comments:
Post a Comment