Rails — A faster way for next and previous links on a post, article, or any model

Stamped: 2012-01-12 00:00:00 -0500 | Show comments

This is a simple, but common question I come across when dealing with people learning rails, as they inevitably do a bloggish type app first.

I assume you have a model like the one below

class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body


Adding the next/prev links

Personally, I prefer to add instance methods as well as a scope or a class method. Check it out below:

class Article < ActiveRecord::Base
    scope :next, lambda {|id| where("id > ?",id).order("id ASC") } # this is the default ordering for AR
    scope :previous, lambda {|id| where("id < ?",id).order("id DESC") }

    def next

    def previous

    #### used below to benchmark
    # scope :next_by_date, lambda {|created_at| where("created_at > ?",created_at).order("created_at ASC") } # this is the default ordering for AR
    # scope :previous_by_date, lambda {|created_at| where("created_at < ?",created_at).order("created_at DESC") }

    # def next_by_date
    #  Article.next_by_date(self.created_at).first
    # end

    # def previous_by_date
    #  Article.previous_by_date(self.created_at).first
    # end


How it works

If you're confused, don't be! Let's assume we have an array of article ids: [1,2,4,6]. If we're looking for the next article for 2, it would obviously be 4. Programmatically, you can't just say Article.find(id+1), because it won't exist in some situations where articles are deleted.

We have to find the next id above 2, hence where("id > ?", id). However, if you order the ids descending you'll get 6, because 6 is the highest in the list while still being larger than 2. Knowing that, we then sort the list ascending, because 4 will be first on the list when ordering ascending.

This, of course, assumes you're using the instance methods as I did, if you used .last instead, the ordering for each would be reversed.

It's slightly faster than sorting by a date, or any other field not indexed

Since you're sorting by the primary key, it should be much faster than sorting by any other field, as the primary key is usually indexed in most databases. Plus, it's computationally easier to sort an integer, however slightly.

For example:

sql = "INSERT INTO articles (title, body, user_id, created_at) VALUES "
array = []
n = 100000
n.times do |i| 
  array << "('#{i}','#{i}', 1, '#{eval("#{i}.days.ago")}')"
sql += array.join(",")
Benchmark.bm do |x|
  x.report { Article.first.next_by_date }
  x.report("by date: ") { Article.first.next_by_date }
  x.report { Article.first.next }
  x.report("by id: ") { Article.first.next }

I ran each twice just incase some caching occurred, and the results were:

user     system      total        real
by date:   0.000000   0.000000   0.000000 (  0.017141)
by id:     0.000000   0.000000   0.000000 (  0.000682)

Changing the repetition will demonstrate a linear pattern, say O(n), for finding the next by, whereas by date will demonstrate a more exponential pattern, perhaps nearing O(n^2).

tags: rails
recent entries
Rails — A faster way for next and previous links on a post, article, or any model
The awkward things Siri says
Node.js — Getting oAuth Up and Running Using Express.js and Mongoose
Node.js — Getting oAuth Up and Running Using Express.js, Railway.js and Mongoose
Migrating from Rails 3.1 RC4 to RC5 using Heroku's Cedar Stack (also compass, unicorn, and sendgrid)
Random Freeze Fix for GTX 460 in 10.6 (osx86)
Wasted on Steam - an analytic tool for the Steam platform
Rails 3.1 — SQL logging to STDOUT during testing (with rspec, test::unit, or cucumber)
Rails 3.1 — Using ERB/HAML/etc within a Coffeescript JS file
Rails 3.1 — 'load_missing_contant': Expected ... to define ... (LoadError)
View the entire archive of articles