When a complex filtering solution is required to be built the code becomes a mess more often than not. Filtering records is a pretty simple task on it’s own: just apply required ActiveRecord scopes on a certain conditions of the given params. Those conditions, however, can become much more complex than you expect. As a result, the code spreads out in controller, models and logic layers making it hardly readable and maintainable. Further down, I’ll describe a simple approach that’ll help avoid such annoyances.First off, we want to delegate all the filtering, ordering, pagination and other collection presentation stuff to a separate class so that controller code became as simple as possible (example code uses
CanCan gem, which is augmenting collection pattern usage):
class LeadsController < ApplicationController
def index
# current_ability – CanCan method
collection = LeadsCollection.new(current_ability, current_user, params)
@leads = collection.eager_loaded.paginated.items
end
end
I assume that this is not a single occurrence of collection functionality in the system. So, common behavior can be extracted to some base class:
class BaseCollection
attr_reader :ability, :user, :params
module Authorization
def items
# CanCan stuff
super.accessible_by(ability)
end
end
module Ordering
def items
if order_key
super.order(ordering_arel)
else
super
end
end
private
def ordering_arel
self.arel_table(order_key.to_sym).send(order_dir)
end
def order_key
params[:order_key]
end
def order_dir
params[:order_dir] =~ /\Adesc\z/i ? :desc : :asc
end
end
module Pagination
def items
super.page(params[:page]).per(params[:per])
end
end
module Search
def items
# Assuming search_for is implemented for model
super.search_for(params[:search])
end
end
end
Here comes
Criteria Pattern in play. The idea is to encapsulate collection aspects into small manageable pieces of code and chain them by means of extending collection object with those modules:
class LeadsCollection < BaseCollection
def initialize(ability, user, params)
@ability = ability
@user = user
@params = params
extend Authorization, Ordering, Pagination, Search
end
end
Certainly, my example above is oversimplified and does not differ from the base class. Let's add some functionality to make this example have a bit more sense:
class LeadsCollection < BaseCollection
module LeadsStatusScope
def items
# Here and below some scopes are applied.
# We assume they all properly implemented in model
super.in_status(params[:lead_status])
end
end
module DepartmentScope
def items
if department_id_given?
super.with_department(department_id)
else
super
end
end
private
def department_id
params[:department]
end
def department_id_given?
department_id && Department.where(id: department_id).exists?
end
end
module RoleScope
def items
params[:role].present? ? super.with_role(params[:role]) : super
end
end
module HasQualificationScope
def items
if params[:has_qualification].present?
super.with_qualification(params[:has_qualification])
else
super
end
end
end
def initialize(ability, user, params)
@ability = ability
@user = user
@params = params
extend Authorization,
Ordering,
Search,
LeadStatusScope,
DepartmentScope,
RoleScope,
HasQualificationScope
end
def items
# default scope for collection can be applied here
Lead.scoped_for_user(user, params[:scope])
end
end
For the sake of making the code reusable (e. g. for reports, exports etc.) some modules can be applied by calling separate method. You can see it in the first code insert for controller: methods #paginated and #eager_loaded. Let's add them to our collection:
class LeadsCollection < BaseCollection
...
def EagerLoaded
def items
super.includes(:details, :matches, :comments)
end
end
...
def eager_loaded
extend EagerLoaded
self
end
def paginated
# Pagination module is defined in BaseCollection class
extend Pagination
self
end
end
Sure thing, you have to be accurate with chaining #items methods. Some things will not work together, e.g. using include for eager loading in one module and defining custom select fields in another and then chaining them together.So, this is it. I hope this collection approach will come in handy in your work.