Rails: Get next / previous record

SqlRuby on-RailsRuby on-Rails-3

Sql Problem Overview


My app has Photos that belong to Users.

In a photo#show view I'd like to show "More from this user", and show a next and previous photo from that user. I would be fine with these being the next/previous photo in id order or the next/previous photo in created_at order.

How would you write that kind of query for one next / previous photo, or for multiple next / previous photos?

Sql Solutions


Solution 1 - Sql

Try this:

class User
  has_many :photos
end


class Photo
  belongs_to :user

  def next
    user.photos.where("id > ?", id).first
  end

  def prev
    user.photos.where("id < ?", id).last
  end

end

Now you can:

photo.next
photo.prev

Solution 2 - Sql

It lead me to a solution for my problem as well. I was trying to make a next/prev for an item, no associations involved. ended up doing something like this in my model:

  def next
    Item.where("id > ?", id).order("id ASC").first || Item.first
  end

  def previous
    Item.where("id < ?", id).order("id DESC").first || Item.last
  end

This way it loops around, from last item it goes to the first one and the other way around. I just call @item.next in my views afterwards.

Solution 3 - Sql

Not sure if this is a change in Rails 3.2+, but instead of:

model.where("id < ?", id).first

for the previous. You have to do

.where("id > ?", id).last

It seems that the "order by" is wrong, so first give you the first record in the DB, because if you have 3 items lower than the current, [1,3,4], then the "first" is 1, but that last is the one you ware looking for. You could also apply a sort to after the where, but thats an extra step.

Solution 4 - Sql

class Photo < ActiveRecord::Base
  belongs_to :user
  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
    user.photos.next(self.id).first
  end

  def previous
    user.photos.previous(self.id).first
  end
end

Then you can:

photo.previous
photo.next

Solution 5 - Sql

This should work, and I think it's more efficient than the other solutions in that it doesn't retrieve every record above or below the current one just to get to the next or previous one:

def next
  # remember default order is ascending, so I left it out here
  Photo.offset(self.id).limit(1).first
end

def prev
  # I set limit to 2 because if you specify descending order with an 
  # offset/limit query, the result includes the offset record as the first
  Photo.offset(self.id).limit(2).order(id: :desc).last
end

This is my first answer ever posted on StackOverflow, and this question is pretty old...I hope somebody sees it :)

Solution 6 - Sql

You can pass some options into the where method:

For the next photo:

Photo.where(:user_id => current_user.id, :created_at > current_photo.created_at).order("created_at").first

Previous photo

Photo.where(:user_id => current_user.id, :created_at < current_photo.created_at).order("created_at").last

I may have the first/last mixed up.

Solution 7 - Sql

Modify your app/models/application_record.rb to the following code:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def next
    self.class.where("id > ?", id).order("id ASC").first || self.class.first
  end

  def previous
   self.class.where("id < ?", id).order("id DESC").first || self.class.last
  end
end

Then you can use next() and previous() in all your models.

Solution 8 - Sql

You might want to check Nexter. It works on any dynamically created scope instead of relying on one hardcoded in your model.

Solution 9 - Sql

class Photo < ActiveRecord::Base
  belongs_to :user

  default_scope { order('published_at DESC, id DESC') }

  def next
    current = nil
    user.photos.where('published_at >= ?', published_at).each do |p|
      if p.id == id then break else current = p end
    end
    return current
  end

  def previous
    current = nil
    user.photos.where('published_at <= ?', published_at).reverse.each do |p|
      if p.id == id then break else current = p end
    end
    return current
  end
end

I found that the answers already here did not serve my case. Imagine that you want a previous or next based on the date published, but some photos are published on the same date. This version will loop through the photos in the order they are rendered on the page, and take the ones before and after the current one in the collection.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAndrewView Question on Stackoverflow
Solution 1 - SqlHarish ShettyView Answer on Stackoverflow
Solution 2 - SqlCremzView Answer on Stackoverflow
Solution 3 - Sqljwilcox09View Answer on Stackoverflow
Solution 4 - SqlZernelView Answer on Stackoverflow
Solution 5 - Sqljinstrider2000View Answer on Stackoverflow
Solution 6 - SqlJordan SchwartzView Answer on Stackoverflow
Solution 7 - SqlOldwolf PangView Answer on Stackoverflow
Solution 8 - SqlcharlysistoView Answer on Stackoverflow
Solution 9 - SqlNealJMDView Answer on Stackoverflow