Format-specific server errors for your Ajax app using Merb
8 days ago / 0 Comments
Handling server-side errors in Ajaxy Web apps has always been annoying. There is a raft of articles on the subject, and I've seen it brought up in numerous conference talks, too.
In lots of articles, the idea of 'handling' the error is limited to printing some scrap of text for the user (often in our favorite Philip-Glass-mimimalist piece of 'user interface,' the alert box). Super-cool, you're trying to cram something that usually occupies an entire Web page into a postage-stamp-sized square.
It's just not helpful getting back a bunch of formatted HTML instead of the data your Ajax UI is expecting.
What flavor of FAIL?
When something goes south on your server, you need to tell your clients a bit more than "oh, shit." Server-side FAIL comes in multiple flavors, just like 'Cokes' here in Texas. ("What kind of Coke do you want?" Yeah, it's silly, but I still giggle anytime I hear someone say 'pop' or 'soda.')
And as more Web clients begin to implement more RESTful interfaces, and implement them in more depth, this question of what kind of failure starts to be even more important, because there's a broader range of errors to consider, and they don't always mean there's been some kind of catastrophic breakage.
Looking at the XHR object status (or however you can get at the HTTP status code), that's a good start, but that doesn't really tell you that much about what exactly screwed up, or why -- so there's no real way to know what to do about it, or what to show to the end-users.
Hammered error-detail doo-doo
Your next problem then is how to hammer the error details into something usable for your app, and worthwhile for the end-user.
If your Ajax app requests some data, and the request succeeds, you get a nice string of parsable JSON data your code can consume -- but if something goes wrong on the server and you get back ... a big page of HTML markup. To quote Miracle Max: "While you're at it, why don't you give me a nice paper cut and pour lemon juice on it?"
In the early Ajax days, one method I used a lot for debugging was popping up an entirely new browser window and dumping the responseText into it. It's a pretty reasonable approach for development (until Firefox's new-window-in-a-tab breaks it, thanks, guys) but it's not really helpful for end-users in a production app.
Another fun 'solution' involves doing string comparisons or regex checks against that massive HTML hairball. Kind of like groping around incoherently for where you dropped your car keys at the party after your fifth beer. Not just sub-optimal -- more like anti-optimal.
Nice work if you can get it
If your client is really integrated with your server, you can tailor your error responses and client-side handling to work together. Nothing like writing everything yourself on both sides to make sure your shit actually works.
Chandler server returns errors in Atom format, so when I worked at OSAF I could count on JavaScript having access to a nice, brief message to display to end-users -- as well as more detailed info (a big ol' Java stack trace) that we could give the user a link to in the error dialog.
Unfortunately we're out of 'zardoz.xml'
What if you're not so lucky? What if you have to support different, disparate clients, and multiple data formats? Shouldn't your app provide error data in those same formats?
If your local Vietnamese restaurant can take your order in English, then it's pretty reasonble to expect they can tell you in English that they're out of Sriracha sauce or whatever. (Like that would ever happen at any self-respecting Vietnamese place, right -- anyhow, you get the point.)
So if I ask for an XML file that turns out not to exist, it'd be pretty kick-ass to get back some XML that tells me so.
Doing it with Merb
I've been doing a lot of work lately with Merb, the Ruby Web-app development framework. It has pretty smart content-negotiation support, which makes it super-easy to provide data in multiple formats. It's also really customizable, so it turns out to be a snap in Merb to do format-specific error handling.
All you have to do is make some changes to the Exceptions controller in /app/controllers/exceptions.rb, and you can return errors to the client in whatever format the original request was in.
Here's the code:
class Exceptions < Application include Merb::ResponderMixin provides :json, :xml # handle NotFound exceptions (404) def not_found render_for_format end # handle NotAcceptable exceptions (406) def not_acceptable render_for_format end def render_for_format format = content_type if format == :html render :format => :html else except = params[:exception] # Status code, e.g., 404 stat = except.class::STATUS # Status text, e.g., "Not Found" words = except.name.split('_') words.each do |w| w.capitalize! end stat_text = words.join(' ') # Error message, e.g., "Controller 'Zardoz' not found" msg = except.message data = { :status => stat, :status_text => stat_text, :message => msg } begin display data # Handle formats the error-handler doesn't know about rescue NotAcceptable render :format => :html end end end end
A few examples
So a client making requests for a non-existent endpoint of "zardoz," in various formats, would see something like these:
/zardoz.json
{"status":404,"message":"Controller 'Zardoz' not found","status_text":"Not Found"}/zardoz.xml
<?xml version="1.0" encoding="UTF-8"?> <hash> <status type="integer">404</status> <message>Controller 'Zardoz' not found</message> <status-text>Not Found</status-text> </hash>
Content-negotiation goodies
First of all, you have to include ResponderMixin so you can get access to Merb's content-negotiation goodies. Then it's pretty simple to pull together the data you want to include in the response, and spit it out in the desired format.
"I'm sorry, I don't speak FOAF"
The rescue for the NotAcceptable exception is kind of important. I guess it's a bit meta, but if your exception controller itself doesn't provide the requested format, you need to be able to fall back to a plain HTML-format error page.
You might wonder why you'd even bother trying to provide format-specific errors for the not_acceptable action (this is where you end up when the client requests a format that's not supported). Well, there may be cases where your app speaks a particular format, but the requested controller/action does not. In that case, you can tell the client -- in the requested format -- that the requested format isn't supported. Fun.
It's a bit like the French dude I met in Thailand who responded to my question with "I'm sorry, I don't speak English" -- in what seemed to me to be perfect English. At least with the server-side error messages you know it's nothing personal -- and it's probably not because you're an ugly American.
Caveats
By default Merb doesn't actually bother parsing requests when they don't match any routes. This makes perfect sense of course -- but it means there's no content_type for those requests, so you can't serve up your format-specific NotFound errors for those.
If you want to have format-specific errors for all 404s, even for those that don't match any known routes, you can enable the default routes in /config/router.rb.
Okay, wrap it up, buddy ...
Format-specific errors are a nice tool in the toolchest -- getting data in the format you asked for, even when something goes wrong, is kind of nice. Dealing with errors in your Ajaxy Web apps is annoying enough without having to trawl through a big pile of markup in an error HTML page.
As you can see from the code above, Merb makes this super-simple to do, but I'm sure there are all kinds of good ways to accomplish it in your language/framework of choice.
Kicking the tires on SproutCore, not keen on the object literals
16 days ago / 2 Comments
Started kicking the tires this evening on SproutCore, Apple's "Cocoa-inspired" JavaScript framework for buildling Web applications.
It looks pretty solid so far, although not nearly as far along as some of the older frameworks like Dojo, YUI, or even JQuery. The docs look pretty good, and rather than trying to take a server-language-agnostic approach, it integrates tightly with Ruby, which is personally appealing for me -- I like Ruby a lot, and my job at Seesmic has me doing a lot of server-side stuff in Ruby.
The Hello World tutorial is nicely done -- it does a good job of demonstrating how easily you can get a dynamic Web UI using some Ruby helpers in Erubis rhtml templates.
SproutCore works hard to provide some MVC-sanity for your Ajaxy Web app project, so files are organized logically into separate directories. Your JavaScript controller logic is kept in .js files in a controllers directory.
The only thing I've found remotely disagreeable so far with SproutCore is that, like the other Ruby-friendly library, Prototype, the syntax leans heavily toward JavaScript object literals for pseudo-class definitions.
You create your controller by passing an object containing the properties and methods desired to SC.Object.create, like so:
HelloWorld.appController = SC.Object.create( /** @scope HelloWorld.appController */ { greeting: "Hello World!", toggleGreeting: function() { var currentGreeting = this.get('greeting'); var newGreeting = (currentGreeting === 'Hello World!') ? 'I am on SproutCore!' : 'Hello World!'; this.set('greeting', newGreeting); } });
I'm not keen on this syntax for creating objects for a couple of reasons.
One is the small-but-irritating trailing-comma problem with object literals. Some browsers, like Firefox, don't really mind them. Internet Explorer's JScript parser chokes on them like a fraternity pledge on his eighth shot of tequila.
When I lived in Japan, there was this local pub with a door that was ridiculously short -- the top of it hit me right around chest-level (I'm not a tall man), and you had to remember when entering and exiting to bend down to get through the door. I bumped my head on that thing time after time (as did all the locals -- it wasn't just a gaijin thing), cursing every time because I knew to duck my head.
The trailing-comma-separator problem in JavaScript is the same kind of problem as that goofy door -- you keep banging your head on it, even though you should know to be careful with it. Avoiding those object literals for something as ubiquitous as declaring your pseudo-classes makes it a lot less likely you'll be smacking your head on it over and over.
The other big downside of the object-literal syntax for pseudo-classes is that you don't get any local scoping, so you're reduced to using the underscore-propery-name convention to fake private variables. They're still public members, but you just pretend they're private. It's not unworkable, but it's much nicer to have access to actual private variables in your code.
There are a couple of alternatives to the object literal. You can use function objects in a couple of ways:
var foo; // 1. Object literal foo = {}; // 2. Execute a function that returns an object foo = function () { return {}; }(); // 3. Use the 'new' keyword, return an obj by default foo = new function () {};
All of those will return an object. Number two gives you private variables, but the final object you pass back still has to use the annoying object-literal syntax. Not much of a win.
Number three gives you private variables, and lets you use the normal idiomatic "this" syntax you're used to from writing plain old OO JavaScript.
Let's see how SproutCore's Hello World tutorial might turn out using the inline constructor function syntax from number three:
HelloWorld.appController = SC.Object.create( /** @scope HelloWorld.appController */ new function () { var _strings = { HELLO: "Hello World!", SPROUT: "I am on SproutCore!" }; this.greeting = _strings.HELLO; this.toggleGreeting = function () { var currentGreeting = this.get('greeting'); var newGreeting = (currentGreeting === _strings.HELLO) ? _strings.SPROUT : _strings.HELLO; this.set('greeting', newGreeting); }; });
Notice I added a private variable, _strings, to the controller pseudo-class. (I name private vars with a leading underscore to make it clear in my code that it's not a plain local variable -- but it's a real private variable.)
This also looks a lot better to me -- it's a wee bit more verbose with the repeated "this," but it looks to me more like the plain idiomatic JavaScript I'm used to, and there's no worry about smacking your head on that trailing-comma-separator problem.
SproutCore looks pretty reasonable so far. I hope to post more as I get more familiar with it.
Comments are back -- die, spammer asshats
17 days ago / 2 Comments
Just added a bit of anti-spam fu to the comments field. Not sure how well it will work, but I'll give 'er a go and see.
Comments are back, as long as the spammers stay gone.
Alex Russell's talk at Google I/O 2008 -- comments, nits, and a quibble
17 days ago / 0 Comments
Just sat down and watched Alex Russell's talk at Google I/O (from the end of last month) on the state of the browser, entitled Can We Get There From Here? As always, Alex did a great job of weaving the disparate factors in play (and the weird, tweaky specifics that come along with them) into an interesting and understandable whole.
A few comments I thought worth jotting down -- maybe nits to pick.
Where is "there"?
In the talk, as in previous blog posts, Alex talks about all of us Web devs wanting "new stuff in the browser," and wondering who we need to bug to get it in there. He rightly points out that we can't count on the standards bodies to drive the process of innovation forward.
I'd like to qualify the Christmas wish-list desire for "new stuff" in the browser by pointing out that what we all really want is "the same new stuff, in all the different browsers the users actually use."
The competition from multiple browsers is messy -- and granted, the standards bodies are an actual drag on the innovation process -- but they force the various players to decide together which "new stuff" goes in, and what that new stuff looks like.
The alternative is the monoculture like Microsoft gave us in IE (and, one could argue like Adobe/Microsoft are trying to do with Flash/Silverlight), that lets us do a lot of cool stuff really quickly and conveniently -- until the last credible shred of competition is killed off, and we see years pass with no new features appearing.
In the talk, Alex mentions in passing that he wonders what such a monoculture would look like if it were an open-source browser.
I wonder if such a thing is even possible, given the openness, and the freedom (or, some might argue, the constant urge) to fork inherent in the open-source development paragdigm. All it takes is a couple of really smart guys with an opinion to create credible competitive pressure -- and we all know there's no shortage of those guys in open source. GNOME or KDE? Ubuntu or Fedora? Suse? Everybody's got a slight variation of the idea that they think is better than the others.
How fast are we getting there?
We all feel the need for speed -- that satisfaction of taking advantage of a cool new feature we haven't gotten to use before in our apps. "Oooh, look at the shiny!"
But it's worth pointing out that one of the reasons Web development has become so ubiquitous, and gathered such momentum behind it, is paradoxically because of that weirdly long period of statis after Microsoft squashed Netscape, when IE stagnated.
I was building browser-based app UIs during the initial round of browser wars, and it was kind of confusing keeping up with the browser revs and their competing, kind-of-overlapping, non-standard new features.
I don't exactly get all misty-eyed at the thought of all those "document.layers vs. document.all" branches in the code, and I'm a even a guy who's actually excited to get the "new stuff." Think about the corporate IT types -- in large organizations, change is expensive, no matter if some guy is telling you there's a big load of awesome in that change.
In some big companies, even rolling out a new version of IE is a huge deal (Hey Oracle, are y'all still sticking with IE6?) -- back in my days at KnowledgeWire I can remember dealing with the inevitable surly corporate IT types who balked at the idea of even rolling out a new version of the Shockwave plugin. Not a whole new frigging browser, just a plugin.
Auto-update is great and all, but behind a big corporate firewall, all bets are off, because change is a pain in the ass for these guys.
Again, the standards bodies aren't exactly playing Prometheus of Innovation, bravely bringing the fire of "new stuff" to mortals, but they provide a safe, measured rate of change that the IT guys can point their bosses to -- "Hey look, this 'JSON' thing is an IETF standard." With that to point to, the likelihood is lower they'll get shitcanned if something horrible and unforeseen happens during the change.
View Source as the signifier
Alex mentioned the idea of Ctrl/Cmd+U for viewing the document source as a possible benchmark for conformance with the idea of "Webishness," contrasting that with Flash/Silverlight apps that are basically big ol' binary black-boxes.
I think it's worth being a little more specific and possibly replacing the simple View Source benchmark with the idea of "decomposability."
Even Ajax-heavy Web apps with nothing but "head, script tags, body" are stlll put together out of plaintext, and can be taken apart pretty easily to see how they tick, using either normal text editors, or freely available browser extensions like Firebug or the Web Developer Toolbar.
Sure, there's View Source -- but there's also View Generated Source. Nothing like that exists in Flash/Silverlight Land. (Okay, so it's really a continuum with stuff like decompillers way, way, way at the other end, but those aren't things you can expect normal humans to use.)
That kind of decomposability is actually another thing that fosters the development of that bustling bazaar of workaday devs that Alex talks about, making all that noise at the base of the ivory tower and irritating all the hardcore-developer dudes inside.
In fact, the simple fact of being able to take stuff apart with free tools gives any of those workaday dudes with half a clue and some persistence (okay, and the ability to read and follow instructions) entre into that tower.
That's right, you don't have to have a degree, or know the secret handshake or anything, to do this stuff (douchebag comments from disgruntled drama-queen developers notwithstanding). In fact, you can even have an English degree, or something stupid like that.
So the real importance isn't whether there's a lot of stuff right on the page to look at when you do Ctrl/Cmd+U -- sure, that's an easy and obvious indicator of the height of the barrier to entry -- but a more meaningful question is "can you take it apart?" and "how much do the tools cost?"
Conclusion
Alex really did an incredible job of distilling a bunch of really complicated stuff down into something digestible. If you have about an hour, and are interested in where we're headed with browser-based apps, you owe it to yourself to check out his talk.
A final quibble
NT4 actually shipped with IE2, not IE3. I don't remember anyone actually using it, but that's what was on the installation CD.
Comments are shut down
17 days ago / 0 Comments
Comments are currently down because of the usual spammer asshats posting spurious comments to plug the usual bizarre shit you'd never imagine anyone would actually buy.
PmpknPi is nicely minimalist, so it should be easy to hack in some kind of spammer-asshat protection -- if someone hasn't already.
Up and running with PmpknPi
2 months ago / 0 Comments
Time for a change -- I've been needing to move my blog to a different server for awhile now, so I'm using this opportunity to change from Typo (a Rails-based blog platform) to PmpknPi, which advertises itself as "A RESTful Blog API written in Merb."
If you've a techie who's been hiding under a rock, and don't know what Merb is, it's an MVC Web-app development framework written in Ruby. It's basically a competing/complementary framework to Rails -- much more minimalist and performance-oriented.
I'm running this blog on Merb with the Thin Ruby Web server, a small, fast, event-driven HTTP server written in Ruby.
Merb is what we're using to build a lot of the Seesmic Web app, including the Ajax Web UI that's my primary focus.
We've been hard at work at Seesmic building out our REST API, which at least in part explains the dead air on this blog. (Twitter is probably another good target for finger-pointing on that count.)
Anyhow, with the switch of this blog to PmpknPi, I lose a lot of the bloat of both Rails and Typo, and should have something a bit more hackable. I'll also get something I should have put a premium on long, long ago -- code syntax highlighting from the excellent CodeRay library. 'Bout frigging time.
OSAF 2.0 to Seesmic pre-alpha
5 months ago / 1 Comment
On Tuesday of this week, OSAF announced a significant restructuring of the organization. OSAF is now moving forward into a new phase of its existence with a smaller team -- a team that I will not be a part of.
I'm extremely proud of my work at OSAF, and what our small team was able to accomplish on Chandler Server in such a short time. However, I had been thinking lately that it might be time to consider other possibilities -- so when the reorg began, I decided OSAF would be better served by letting me go, and keeping people who are tightly focused on the goals of the project.
My life has been incredibly enriched by all of the people I've worked with at OSAF, and I'm proud of the work I've done -- both on Chandler Server, and on the Windmill Web-app testing framework. I will of course be following the progress of Chandler with great interest, and will be offering whatever help or assistance I can to the Chandler developers and community.
I'm sure it won't take long for my fellow former OSAFers to get snapped up by various cool ventures. Bay Area tech companies are likely all salivating at the prospect of these brilliant people now out looking for jobs.
As for me, thanks to a kind introduction from one of those ex-OSAFers, Mike "Bear" Taylor, I am pleasantly surprised to find myself already employed, working on something new and pretty damned exciting. Starting January 16th, I will be working at Seesmic, a startup company doing some very interesting stuff with Web video.
The service is still pre-alpha. (no, I don't have any invitation codes yet :)), and I'll be working on an Ajaxy Web UI for the service. I'll be blogging about the new job as things develop -- it's a startup, I'm hoping and expecting that things will be moving quickly.
Updated: here are some links to the various news items regarding OSAF's reorg:
Fleegix.js JavaScript Toolkit 0.4.1 release
5 months ago / 0 Comments
Somehow amidst all the chaos that is currently my life, I've packaged up version 0.4.1 of Fleegix.js for release. You can download the new release at the usual location.
This is primarily a bugfix release. Here are some of the fixes and improvements:
- Fixes in the hash plugin for Safari 2's ridiculous Math.random bug
- Updates to the xml parser plugin to handle CDATA and comment nodes properly
- Fixes for multi-selector class attributes with
css.addClass - Improved caching in timezone-enabled Date plugin, new
getTimezoneAbbreviationmethod - Fixes for previous-year inclusion in rule-hits in the timezone-enabled Date plugin
- New option to squelch errors from execution of handlers in
event.listen - A bunch of new unit tests that work with Windmill's JavaScript test framework
PHP is the McDonald's of programming languages
5 months ago / 0 Comments
Tim Bray has a nice post that succinctly describes the situation with PHP in 2008 -- why it's still popular, but its growth continues to slow.
I think that the various pluses and minuses could be reduced even more by simply thinking of PHP as the McDonald's of programming languages.
PHP, like McDonald's, is really, really convenient, -- it's pretty much already installed everywhere you go. Deployment, like eating at Mickey Dee's, is a breeze.
And maybe I'll be totally ostracized for saying it, but I don't particularly mind using PHP to set up a quickie Web site with some simple templating. It's dead simple, and I know stuff will just work.
Like the food at McDonald's, using PHP isn't that bad every once in a while -- but I wouldn't want to eat it every day. And I'm pretty sure a steady diet of it will kill you.
Rails polymorphic associations and migrations
6 months ago / 0 Comments
In a Rails app I was recently working on, the data model called for associating sets of related business objects. Initially it looked like I was going to have to handle it with sub-classing.
Sub-classing and prog-rock keyboardists
I was kind of reluctant to do this in a Rails app, because the traditional Rails approach to representing an object hierarchy uses single-table inheritance -- dumping a column for every possible attribute for every possible sub-class into one table.
This kind of ugliness is an example of the Rick Wakeman Brute Force Approach: no worries about those primitive synths that can only provide one patch at a time, just drag an ass-load of them with you whenever you go on the road. It gets the job done, but it's not particularly elegant.
In my particular case, sub-classing might have actually worked, if I could have endured the single-table-inheritance stink. But what about cases you may want to group types of things in your app that have a less obvious relationship than can be represented with a hierarchy chain?
Promiscuity is so much nicer
Thankfully, a very nice way to do this sort of thing exists in the form of polymorphic associations (also called "promiscuous associations," yay!), which allow you a lot of the same capabilities as an inheritance hierarchy, but gives you more flexibility in creating relationships between objects.
With polymorphic associations, you can literally connect any type of thing in your data model to any other type. A common example used to describe this is tagging, where a given tag could potentially be associated with any type of thing in your data model.
It almost makes you a little drunk with power, this newfound ability to link this to that and that to the other thing over there. You can almost imagine, with all due apologies to the Pythons, a Royal Society for Associating Things With Other Things.
The hasmanypolymorphs plugin
Using polymorphic associations without any special help is possible in Rails, but it is somewhat limited. I found that what I needed to do required the use of the excellent hasmanypolymorphs plugin developed by Evan Weaver.
If you find yourself reading the docs and getting utterly confused the first time through, as I did, you can check out Pratik Naik's helpful tutorial that does a really nice job of laying it all out.
Dude, where's my DB?
The one piece I found missing in all the docs and tutorials for the hasmanypolymorphs plugin was anything that showed what the corresponding database tables should look like, or how to use Rails migrations to set them up.
(There is a helpful, albeit minimal, example in the plugin source code examples/ directory for a simple single-sided association that pointed me in the right direction.)
I figured it might be helpful for other people using the plugin to see some migration examples. It might save them some of the time I spent futzing with things to get it all working.
Single-sided associations
Single-sided polymorphic associations are the simpler of the two kinds, where many types of things are associated with a single other type.
Say I'm writing an e-learning app, and I have courses, exams, and seminars, all of which might be assigned to a person to take. My model would look like this:
class Course < ActiveRecord::Base end class Exam < ActiveRecord::Base end class Seminar < ActiveRecord::Base end class TasksPerson < ActiveRecord::Base belongs_to :person belongs_to :assignment, :polymorphic => true end class Person < ActiveRecord::Base has_many_polymorphs :assignments, :from => [:courses, :exams, :seminars], :through => :tasks_people end
And the migration would look like this:
class TasksPeople < ActiveRecord::Migration def self.up create_table :tasks_people do |t| t.column :person_id, :integer t.column :assignment_id, :integer t.column :assignment_type, :string end create_table :people do |t| t.column :name, :string end create_table :courses do |t| t.column :name, :string end create_table :exams do |t| t.column :name, :string end create_table :seminars do |t| t.column :name, :string end end def self.down drop_table :tasks_people drop_table :people drop_table :courses drop_table :exams drop_table :seminars end end
Once you've got that set up, you can try it out in the console to see if stuff is working:
>> c = Course.create(:name => "How Not To Get Eaten By An Alligator") >> e = Exam.create(:name => "Zombie Army Attack Preparedness") >> p = Person.create(:name => "Ravi Shankar") >> p.assignments.push(c) >> p.assignments.push(e) >> p.assignments.length => 2
Woohoo, Ravi has two assignments, just like you'd expect.
The beauty of this is that now I can assign courses, exams, seminars -- or anything else I deem "tasky" -- to people in my app, without having to wrangle the unwieldiness of shoving all that taskiness into a single table.
Double-sided associations
This is where it gets all wacky -- where multiple different types of things can be associated with other, multiple different types of things. Mass chaos ensues -- but in a good way.
Imagine now that e-learning app has grown and I need to make more complicated kinds of assignments. I need to be able to assign those different types of tasks (courses, exams, seminars) to individuals, or to groups -- say, for example, to people, jobs, and organizations.
(For anyone who worked with me back in the KnowledgeWire days, this is a nice trip down memory lane.)
This is a good example of a case where simple sub-classing would be conceptually awkward and weird -- people, jobs, and entire organizations aren't particularly similar things. The only thing they really have in common in this data model is their ability to have things assigned to them.
The model code would look like this:
class Course < ActiveRecord::Base end class Exam < ActiveRecord::Base end class Seminar < ActiveRecord::Base end class Person < ActiveRecord::Base end class Job < ActiveRecord::Base end class Organization < ActiveRecord::Base end class AssignmentsAssignee < ActiveRecord::Base belongs_to :assignment, :polymorphic => true belongs_to :assignee, :polymorphic => true acts_as_double_polymorphic_join( :assignments =>[:courses, :exams, :seminars], :assignees =>[:people, :jobs, :organizations] ) end
And the migration would be like this:
class AssignmentsAssignees < ActiveRecord::Migration def self.up create_table :assignments_assignees do |t| t.column :assignment_id, :integer t.column :assignment_type, :string t.column :assignee_id, :integer t.column :assignee_type, :string end create_table :people do |t| t.column :name, :string end create_table :jobs do |t| t.column :name, :string end create_table :organizations do |t| t.column :name, :string end create_table :courses do |t| t.column :name, :string end create_table :exams do |t| t.column :name, :string end create_table :seminars do |t| t.column :name, :string end end def self.down drop_table :assignments_assignees drop_table :people drop_table :jobs drop_table :organizations drop_table :courses drop_table :exams drop_table :seminars end end
Now let's hop back into the console and see if this stuff all works:
>> c = Course.create(:name => "Life and Times of the Three Stooges") >> s = Seminar.create(:name => "LARPing for Fun and Profit") >> p = Person.create(:name => "Henry Kissinger") >> o = Organization.create(:name => "Australopithecus Unlimited") >> p.assignments.push(c) >> p.assignments.push(s) >> p.assignments.length => 2 >> o.assignments.push(c) >> o.assignments.push(s) >> o.assignments.length => 2
Very nice. I can give the same assignments to both Henry Kissinger and to the entire Australopithecus Unlimited organization.
I can assign any of these tasky things to a person, job, or organization in the app. And also interestingly, I can do the reverse:
>> e = Exam.create(:name => "Apple Fanboy Test") >> e.assignees.push(p) >> e.assignees.push(o) >> e.assignees.length => 2
Gotchas
One gotcha I noticed was that migrations wouldn't run to set up the database properly with the hasmanypolymorphs plugin loaded. Looks like it's some kind of circular dependency issue. In any case, leaving the plugin require statement out of environment.rb until after setting up the DB is a workaround for this problem.
Wrapping up
There you have it. A nice, flexible way to create associations between loosely related types in your model. No ugly sub-classing stuff by trying to shoehorn every related type into one table.
There are some other nice tutorials and docs online to help you, but I couldn't find any that include the migration scripts above. I hope this helps save somebody some time.