{"id":6413,"date":"2014-01-22T15:52:35","date_gmt":"2014-01-22T12:52:35","guid":{"rendered":"http:\/\/railsware.com\/blog\/?p=6413"},"modified":"2025-01-31T19:28:14","modified_gmt":"2025-01-31T16:28:14","slug":"criteria-pattern-applied-for-organizing-complex-filtering","status":"publish","type":"post","link":"https:\/\/railsware.com\/blog\/criteria-pattern-applied-for-organizing-complex-filtering\/","title":{"rendered":"Criteria pattern applied for organizing complex filtering"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">When a complex filtering solution is required to be built, the code becomes a mess more often than not. Filtering records is a straightforward task on its own: just apply the required ActiveRecord scopes based on certain conditions of the given parameters. As a result, the code often spreads across controllers, models, and logic layers, making it hardly readable and maintainable. Further down, I&#8217;ll describe a simple approach that will help avoid such annoyances.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"https:\/\/github.com\/ryanb\/cancan\">CanCan<\/a> gem, which is augmenting collection pattern usage):<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class LeadsController &lt; ApplicationController\n  def index\n    # current_ability \u2013 CanCan method\n    collection = LeadsCollection.new(current_ability, current_user, params)\n    @leads = collection.eager_loaded.paginated.items\n  end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class BaseCollection\n  attr_reader :ability, :user, :params\n\n  module Authorization\n    def items\n      # CanCan stuff\n      super.accessible_by(ability)\n    end\n  end\n\n  module Ordering\n    def items\n      if order_key\n        super.order(ordering_arel)\n      else\n        super\n      end\n    end\n\n    private\n\n    def ordering_arel\n      self.arel_table(order_key.to_sym).send(order_dir)\n    end\n\n    def order_key\n      params[:order_key]\n    end\n\n    def order_dir\n      params[:order_dir] =~ \/\\Adesc\\z\/i ? :desc : :asc\n    end\n  end\n\n  module Pagination\n    def items\n      super.page(params[:page]).per(params[:per])\n    end\n  end\n\n  module Search\n    def items\n      # Assuming search_for is implemented for model\n      super.search_for(params[:search])\n    end\n  end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here comes <a href=\"http:\/\/en.wikipedia.org\/wiki\/Criteria_Pattern\">Criteria Pattern<\/a> 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:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class LeadsCollection &lt; BaseCollection\n  def initialize(ability, user, params)\n    @ability = ability\n    @user    = user\n    @params  = params\n\n    extend Authorization, Ordering, Pagination, Search\n  end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Certainly, my example above is oversimplified and does not differ from the base class. Let&#8217;s add some functionality to make this example have a bit more sense:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class LeadsCollection &lt; BaseCollection\n\n  module LeadsStatusScope\n    def items\n      # Here and below some scopes are applied. \n      # We assume they all properly implemented in model\n      super.in_status(params[:lead_status])\n    end\n  end\n\n  module DepartmentScope\n    def items\n      if department_id_given?\n        super.with_department(department_id)\n      else\n        super\n      end\n    end\n\n    private\n\n    def department_id\n      params[:department]\n    end\n\n    def department_id_given?\n      department_id &amp;&amp; Department.where(id: department_id).exists?\n    end\n  end\n\n  module RoleScope\n    def items\n      params[:role].present? ? super.with_role(params[:role]) : super\n    end\n  end\n\n  module HasQualificationScope\n    def items\n      if params[:has_qualification].present?\n        super.with_qualification(params[:has_qualification])\n      else\n        super\n      end\n    end\n  end\n\n  def initialize(ability, user, params)\n    @ability = ability\n    @user    = user\n    @params  = params\n\n    extend Authorization,\n           Ordering, \n           Search, \n           LeadStatusScope, \n           DepartmentScope, \n           RoleScope, \n           HasQualificationScope\n  end\n\n  def items\n    # default scope for collection can be applied here\n    Lead.scoped_for_user(user, params[:scope])\n  end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s add them to our collection:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class LeadsCollection &lt; BaseCollection\n\n  ...\n\n  def EagerLoaded\n    def items\n      super.includes(:details, :matches, :comments)\n    end\n  end\n\n  ...\n\n  def eager_loaded\n    extend EagerLoaded\n    self\n  end\n\n  def paginated\n    # Pagination module is defined in BaseCollection class\n    extend Pagination\n    self\n  end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So, this is it. I hope this collection approach will come in handy in your work.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When a complex filtering solution is required to be built, the code becomes a mess more often than not. Filtering records is a straightforward task on its own: just apply the required ActiveRecord scopes based on certain conditions of the given parameters. As a result, the code often spreads across controllers, models, and logic layers,&#8230;<\/p>\n","protected":false},"author":57,"featured_media":6440,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[3],"tags":[],"coauthors":["Yaroslav Vasilkov"],"class_list":["post-6413","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"acf":[],"aioseo_notices":[],"categories_data":[{"name":"Engineering","link":"https:\/\/railsware.com\/blog?category=development"}],"post_thumbnails":"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2014\/01\/cmplx_filtering_2.png","article_background":"#f1f5fd","amp_enabled":true,"_links":{"self":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/6413","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/users\/57"}],"replies":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/comments?post=6413"}],"version-history":[{"count":28,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/6413\/revisions"}],"predecessor-version":[{"id":18068,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/6413\/revisions\/18068"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media\/6440"}],"wp:attachment":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media?parent=6413"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/categories?post=6413"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/tags?post=6413"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/coauthors?post=6413"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}