Polymorphic Join Tables
Posted on June 29th, 2009 • Filed under Web Design • No Comments
While working on a recent Rails project, I came across the need for a number of polymorphic relationships between models. If you’re not familiar with polymorphisms, a great Railscast can be found here. The inspiration for most of my work came from a helpful tutorial found here.
The problem I encountered involved a feature which would allow the sending of “notices” to users of the site, such that when a user logs in, they can be notified of things by an admin or by other users. A naïve approach would be to simply use a many-to-many relationship between notices and users. However, since users can be accessed through a few other models, I decided to implement a more robust notification system.
Already included in the application is the ability for an admin to create “roles” and “departments” and assign users to them. What I wanted was the ability to create a notice and assign it to all users in a given role or department, as well as to individual users. This may have required three separate join tables, but with a few tricks I was able to reduce it to one. It’s important to note that as of now, Rails cannot handle both polymorphic relations and the “has_many :through” feature. It attempts to create a broken SQL query, and spits out a confusing error. So instead, I went with the plugin has_many_polymorphs.
Here’s what my code looks like:
-
class CreateNoticesUserSources < ActiveRecord::Migration
-
def self.up
-
create_table :user_sinks_user_sources do |t|
-
t.references :notice
-
t.references :user_source, :polymorphic => true
-
end
-
end
-
-
def self.down
-
drop_table :notices_user_sources
-
end
-
end
-
class NoticesUserSources < ActiveRecord::Base
-
belongs_to :user_source, :polymorphic => true
-
belongs_to :notice
-
end
-
class User < ActiveRecord::Base
-
has_many_polymorphs :user_sources, :from => [:departments, :roles, :users], :through => :notices_user_sources
-
end
-
class Department < ActiveRecord::Base
-
end
-
class Role < ActiveRecord::Base
-
end
-
class User < ActiveRecord::Base
-
def users
-
[self]
-
end
-
end
Notice that in the User model, I define the “users” method as an array of the user itself. I do this because we are treating the User model as a “user source,” meaning user.users should return a list of users available to the notice, much like department.users and role.users should. This is a small hack, but allows me to simplify my code later on.
Now that we’ve set up this polymorphic association, I can give the array “notice.user_sources” any combination of departments, roles, and users. I can also use “notice.departments”, “notice.roles”, and “notice.users” to access the subsets of “user_sources.”
Returning a list of unique users assigned to a notice is as simple as writing:
-
notice.user_sources.collect{|s| s.users}.flatten.uniq
The reverse is slightly more complicated (and should probably be implemented in a method in the user model):
-
(user.department.notices + user.roles.collect{|r| r.notices} + user.notices).flatten.uniq
After getting this set-up, I realized that there were other models (notably “restrictions” and “requests”) that should also have a similar relationship with the User model. I could have created a separate set of polymorphic realtionships for each of those models, but instead I decided to create a double join between “user sources” (users, departments, and roles) and “user sinks” (notices, restrictions, and reqeusts). Thankfully, has_many_polymorphs provides a very simple way of doing this:
-
class UserSinksUserSource < ActiveRecord::Base
-
belongs_to :user_source, :polymorphic => true
-
belongs_to :user_sink, :polymorphic => true
-
-
acts_as_double_polymorphic_join(:user_sources =>[:departments,:roles,:users], :user_sinks =>[:restrictions,:requests,:notices])
-
end
The rest follows as one would expect, with polymorphic relationships set up on both ends.
While the double polymorphic join may be more than you would need for most applications (I certainly could have managed without it), it’s nice to know that that kind of functionality is so easy to implement.
One last note, though:
The has_many_polymorphs plugin works well for most purposes, but isn’t a completely pure polymorphic join — it instead defines some hidden methods and performs a few more steps than would ideally be necessary, in order to circumvent the problems rails has with “has_many :through” and polymorphisms. If you set ENV[‘HMP_DEBUG’] to true, you’ll be able to see what is actually going on underneath. It also wouldn’t be too difficult to write similar functionality without the use of this plugin, but it would just add more clutter to the models.
Heroku: Rails in the Cloud
Posted on November 16th, 2008 • Filed under Linux, Web Design • 3 Comments
I recently received an excited email from one of my summer colleagues about a new web start-up called “Heroku.” Essentially, Heroku allows users to develop and test Rails applications from a web interface, keeping all of the essential Rails features and application files accessible and editable from the browser. In addition, users can work locally and update their projects with the Git version control system. The projects are hosted for free by Heroku (potentially with payed subscriptions once it is out of beta), so getting an application up and running is pretty effortless.
this title will change.