Skip to content

Instantly share code, notes, and snippets.

@revans
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save revans/776f90dc59d1787ac07f to your computer and use it in GitHub Desktop.
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.
# 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.
# 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