Rails: Get next / previous record
SqlRuby on-RailsRuby on-Rails-3Sql 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.