Last active
August 29, 2015 14:04
-
-
Save revans/776f90dc59d1787ac07f to your computer and use it in GitHub Desktop.
How to get the Next/Previous Record from an object. I'm using Rails 4.1.4, PostgreSQL 9.3.4 for the examples.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Demo Database Object Schema: | |
# | |
# * name, string | |
# * status, integer | |
# * created_at, datetime | |
# * updated_at, datetime | |
# | |
class Demo < ActiveRecord::Base | |
include BaseScopes | |
# database column to order the previous/next record | |
# defaults to +updated_at+, but you can use any | |
# column from the classes database. | |
column_to_order_by :updated_at | |
# Rails 4 enum for statuses | |
enum status: [ :pending, :active, :inactive, :deleted ] | |
# scopes can be used with the previous/next record as | |
# they are just scopes themselves | |
scope :active, -> { where(status: 'active') } | |
end | |
Demo.next_and_previous_record | |
# => SELECT *, lag(id,1) over (order by updated_at desc) as previous_record, | |
# lead(id,1) over (order by updated_at desc) as next_record | |
# FROM "demos" | |
Demo.active.next_and_previous_record # => An array of demos, that includes a next_demo and previous_demo column | |
Demo.active.first.next_demo # => next demo id accessor | |
Demo.active.first.previous_demo # => previous demo id accessor | |
Demo.active.first.next # => next demo object | |
Demo.active.first.previous # => previous demo object | |
# In the above examples, next/previous demo object are based off the current demo object. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Base Scopes for common SQL queries across many models | |
# | |
module BaseScopes | |
extend ::ActiveSupport::Concern | |
# Setting up some class methods so we can control the column used | |
# for ordering the next/previous objects. | |
# | |
# e.g. | |
# | |
# class Demo < ActiveRecord::Base | |
# include BaseScopes | |
# | |
# column_to_order_by :recorded_on # datetime | |
# end | |
# | |
# The next/previous records will be based on the +recorded_on+ column. | |
# | |
# Added the column_order_by so there is a class level way of accessing | |
# the column used for next/previous ordering. We use this in the | |
# next_and_previous_record scope. | |
# | |
module ClassMethods | |
def column_to_order_by(column) | |
@@column_order_by = column || "updated_at" | |
define_method(:column_order_by) do | |
@@column_order_by | |
end | |
end | |
def column_order_by | |
@@column_order_by | |
end | |
end | |
included do | |
# For each record, created two new dynamic columns (previous_record and next_record), | |
# that will hold the IDs of the next/previous record, based on the column select to | |
# order by. | |
# | |
# The default is set by the +column_to_order_by+ class method. It can be overriden by | |
# passing an argument (a column) to this scope or to the next/previous instance methods. | |
# | |
# WARNING: There is no error checking here (yet) to make sure you are actually passing | |
# a column that does exist. Since a developer needs to implement this, I am | |
# assuming the developer is smart enough to use an actual column that exists | |
# in the database for the given model that this module is included in. | |
# | |
scope :next_and_previous_record, ->(ordering_column = column_order_by) { | |
select("#{model_name.plural}.*"). | |
select("lag(id,1) over (order by #{ordering_column} desc) as previous_record, lead(id,1) over (order by #{ordering_column} desc) as next_record") | |
} | |
# Instance Method so a given object can access the next_* dynamic column. | |
# It uses the model's singular version of the name. | |
# This will return an ID | |
# | |
# e.g. | |
# | |
# Person.model_name.singular # => person | |
# | |
define_method "next_#{model_name.singular}" do | |
read_attribute(:next_record) | |
end | |
# Instance Method so a given object can access the previous_* dynamic column. | |
# It uses the model's singular version of the name. | |
# This will return an ID | |
# | |
# e.g. | |
# | |
# Person.model_name.singular # => person | |
# | |
define_method "previous_#{model_name.singular}" do | |
read_attribute(:previous_record) | |
end | |
end | |
# Instance method to get the next object, using the current object | |
# as the callee. If there is no next object, it will return an | |
# empty ActiveRecord::Relation object as you'd expect in a normal | |
# query. We're just short circuiting the actual database call | |
# based on whether we have an id or not. | |
# | |
# This method explicitly returns an ActiveRecord::Relation array so | |
# the developer can chain addition calls. | |
# | |
def next(ordering_column = column_order_by) | |
next_id = self.send(:"next_#{self.class.model_name.singular}") | |
return self.class.none if next_id.blank? | |
self.class.next_and_previous_record(ordering_column). | |
where( id: next_id ) | |
end | |
# Instance method to get the previous object, using the current object | |
# as the callee. If there is no previous object, it will return an | |
# empty ActiveRecord::Relation object as you'd expect in a normal | |
# query. We're just short circuiting the actual database call | |
# based on whether we have an id or not. | |
# | |
# This method explicitly returns an ActiveRecord::Relation array so | |
# the developer can chain addition calls. | |
# | |
def previous(ordering_column = column_order_by) | |
previous_id = self.send(:"previous_#{self.class.model_name.singular}") | |
return self.class.none if previous_id.blank? | |
self.class.next_and_previous_record(ordering_column). | |
where( id: previous_id ) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment