Rails: ActiveRecord goes :through

Sad geek that I am, I’ve fallen a little bit in love with Ruby on Rails, the application development framework that’s ideal for building database-based web applications.

While Rails is relatively new – version 1.0 was released just last month – there are exciting things just waiting round the corner. I’m in the process of taking our existing UK theatre listings database, which currently runs with PHP and MySQL, and porting it over to Rails. In the meantime, I’ll be taking the opportunity to tidy up the database schema a bit. In doing so, I’m going to be able to take advantage of some new mechanisms only available in Edge Rails – the pre-release version of the platform.

With our database, we have two main types of object to keep track of:

* venues (e.g., the West Yorkshire Playhouse or Menier Chocolate Factory)
* productions (e.g. Phantom of the Opera, a touring production )

For productions that tour a number of venues, we construct a run sheet – a list of venues that the production will be appearing in, alongside the start and end dates of each run. Similarly, venues need a list of productions appearing at their space. In other words, both these relationships between Venue and Product objects can be represented by a Run object.

In Rails code, the three objects – and their immediate relationships with one another – are represented thus:

class Production < ActiveRecord::Base has_many :runs end class Venue < ActiveRecord::Base has_many :runs end class Run < ActiveRecord::Base belongs_to :production belongs_to :venue end So far, so good. It explains the concept in ideological terms. But what really dragged me down in PHP was managing the answers to questions that a web site visitor would have: * What's on at my favourite theatre next month? * I enjoyed this production. Where will it tour next? Maybe my friends around the country should know about it... For the real answers to human questions, we need to have a closer relationship between our Venue and Production objects. Now, in PHP – and, to an only slightly lesser extent, in Rails 1.0 – that would mean dropping down to a pure SQL level. In Rails 1.1, the `:has_many` method has a couple of new parameters that allow us to effectively bridge our intermediate object, the Run, when necessary. ### Talking it :through A basic bridging is as simple as adding a new `:has_many` method on the two main objects, adding a `:through` parameter which specifies the intermediate object. So our code now becomes: class Production < ActiveRecord::Base has_many :runs has_many :venues, :through => :runs # < = new end class Venue < ActiveRecord::Base has_many :runs has_many :productions, :through => :runs # < = new end class Run < ActiveRecord::Base belongs_to :production belongs_to :venue end (We could also add the usual extra conditions, such as an :order parameter, to determine the order in which items get retrieved -- for now, I'll leave them out to make the code as simple as possible.) Now, to get an array of all the productions that a venue has on its books, we can just say: venue.productions However, if a production plays more than once at a given venue – which has been known to happen – we'll get one copy of a Production object for each visit to a venue. If we just wanted one object per production, we can build that in to the relationship: class Venue < ActiveRecord::Base ... has_many :productions, :through => :runs,
:select => ‘DISTINCT *’

Behind the scenes, Rails sorts out the SQL necessary. Very impressive, when you’re used to hand-rolling it all yourself. _(Since this article was originally written, you can also add a `:uniq => true` option, which filters out duplicates after the records have been retrieved. Which method of eliminating duplicates works best for you will depend on your application.)_

But that’s not the best part.

### Association extensions

Our online database extends nearly two years back, and can only get larger. How can we answer some of the following questions?

* I’m going to be staying in Leeds sometime next Autumn. What productions might I be able to see at the West Yorkshire Playhouse?
* Where can I find the touring production of Saturday Night Fever three weeks from now?

A new feature, called association extensions, allows us to define code that only applies in the context of this relationship. Now, bear with me on this: there’s little to no documentation yet (such is the burden of working on cutting-edge technologies) and what there is doesn’t really go into detail. But this evening I had a little epiphany on how it can help me.

Let’s replace the Venue object’s relationship with its many productions, and add a little spice. (Now, this code works for me; it’s probably not the most efficient or most database-neutral. So shoot me, I’m still a learner!)

class Venue < ActiveRecord::Base ... has_many :productions, :through => :runs,
:select => ‘DISTINCT *’ do
def between(start_date = nil, end_date = nil)
date_where = [] if start_date
date_where < < %(runs.ends_on >= ‘#{start_date.strftime(“%Y-%m-%d”)}’)
if end_date
date_where < < %(runs.starts_on <= '#{end_date.strftime("%Y-%m-%d")}') end where_clause = date_where.join ' AND ' find :all, :conditions => where_clause

So now, if we want to present a quick look at all productions that are on at a given venue within a certain time frame, we can in a single line:

@venue = Venue.find_by_name(“Royal Opera House”)
@productions = @venue.productions.between(1.week.ago, 1.month.from_now)

Even better, the same `do…end` block of code could be used in the Production object to filter its Venue objects. Rails allows me to hive this off into a separate module, so both classes can share the same block of code and the same filtering techniques.

Now, you can say there are other ways to do this, and you’d be right. You can argue that stepping over the Run object like this causes problems that going a more ‘traditional’ route would avoid. You’d be right, too, in a way.

For me, though, in this particular case, the Run object is the least important of the three objects in the model. What can be thought of as “conventional” approaches give the run an undue level of priority in the scheme of things. The new features of Rails give me the opportunity to structure my model far more closely to how our customers think of our information. And that can only be a good thing.

Besides, who can’t love this line of code?

@productions = @venue.productions.between(1.week.ago, 1.month.from_now)

Hell, that’s a line of code you can show to management and they’d understand it. That’s priceless.

Author: Scott Matthewman

Formerly Online Editor and Digital Project Manager for The Stage, creator of the award-winning The Gay Vote politics blog, now a full-time software developer specialising in Ruby, Objective-C and Swift, as well as a part-time critic for Musical Theatre Review, The Reviews Hub and others.

5 thoughts on “Rails: ActiveRecord goes :through”