Barefoot Development

Custom Sorting in Ruby

I have been an expert Java developer for many years, but over the past year I've had the opportunity to use Ruby on Rails for a couple real projects. I'm really enjoying Ruby and hope to to use it more often in the future.

My current RoR project has several models with bi-directional has_many :through relationships where two models link to a third join model that has one or more additional property fields. For example, Users link to Albums through a Selections join model. The selections table includes fields for user_id and album_id, with an additional field that contains the rating each user gives to each album. (This example uses different entity names to protect the client.)

These relationships are illustrated (with simplication) here:

class User < ActiveRecord::Base
  has_many :selections, :dependent => :destroy
  has_many :albums, :through => :selections
end

class Album < ActiveRecord::Base
  has_many :selections, :dependent => :destroy
  has_many :users, :through => :selections
end

class Selection < ActiveRecord::Base
  belongs_to :user
  belongs_to :album
end


I needed to be able to list all the albums chosen by a user, sorted by album name. I tried this:

@user.selections.each do |selection|
# ...
end


but the album names were not sorted. I thought that the nifty option :order => 'name' would work on the has_many :through, but alas, it didn't. Then, my Java experience reminded me of the Comparable interface. It turns out that Ruby includes a very similar pattern, but it comes complete with Ruby nice-ness.

When you sort an Array (or anything Enumerable), you can override the <=> method of the Object class to provide your own custom sorting code. (Just like the equals() method in Java.) The <=> method returns -1, 0, or 1 to indicate whether the instance is smaller, equal, or greater than the other object. Here's the method I added to the Selection model to tell it how to sort:

def <=>(o)
   # Compare album name
   album_name_cmp = self.album.name <=> o.album.name
   return album_name_cmp unless album_name_cmp == 0

   # Compare user last name
   user_ln_cmp = self.user.last_name <=> o.user.last_name
   return user_ln_cmp unless user_ln_cmp == 0

   # Compare user first name
   user_fn_cmp = self.user.first_name <=> o.user.first_name
   return user_fn_cmp unless user_fn_cmp == 0

   # Otherwise, compare IDs
   return self.id <=> o.id
end


Then, just change the block slightly to use the sorted version:

@user.selections.sort.each do |selection|
# ...
end


If the model you're trying to sort is not already Comparable, you should add the line include Comparable to include the Comparable mixin. ActiveRecord objects are already Comparable.

Have fun writing your own custom sorting rules in Ruby! Leave a comment if you need more details.

Doug Smith, Senior Developer, Barefoot

Labels: , ,

6 comments

  1. Blogger Joel said:  

    This comment has been removed by the author.

  2. Blogger Joel said:  

    *tries again

    You may be interested in reading this: Sortable has_many :through

  3. Blogger spint said:  

    Can't you just put:
    has_many :selections, :dependent => :destroy, :include => :album

    And then you should be able to use the ":order=>" option, or you can even put ":order =>" in your has_many definition.

  4. Blogger Dr. B. said:  

    @spint: your suggestion would work if I didn't have extra properties in the join table. I actually need the association of the middle table in order to work with its extra fields (i.e., each selection joins a user and an album and includes each user's rating of that album).

    The :order => option didn't work for me in these kind of associations, so that's why I resorted to using Ruby's custom sorting API.

  5. Blogger Artemka said:  

    very useful article. thanks

  6. Blogger Thomas said:  

    very much for this article. It was exactly what I was looking for and worked like a charm!

Post a Comment

« Home