Search code examples
ruby-on-railsrubyactiverecordarel

Rails- Building dynamic query for filtering search results


I am trying to build a dynamic querying method to filter search results.

My models:

class Order < ActiveRecord::Base
  scope :by_state, -> (state) { joins(:states).where("states.id = ?", state) }
  scope :by_counsel, -> (counsel) { where("counsel_id = ?", counsel) }
  scope :by_sales_rep, -> (sales) { where("sales_id = ?", sales) }
  scope :by_year, -> (year) { where("title_number LIKE ?", "%NYN#{year}%") }
  has_many :properties, :dependent => :destroy 
  has_many :documents, :dependent => :destroy
  has_many :participants, :dependent => :destroy
  has_many :states, through: :properties
  belongs_to :action
  belongs_to :role
  belongs_to :type
  belongs_to :sales, :class_name => 'Member'
  belongs_to :counsel, :class_name => 'Member'
  belongs_to :deal_name
end

class Property < ActiveRecord::Base
  belongs_to :order
  belongs_to :state
end

class State < ActiveRecord::Base
    has_many :properties
    has_many :orders, through: :properties
end

I have a page where I display ALL orders by default. I want to have check boxes to allow for filtering of the results. The filters are: Year, State, Sales, and Counsel. an example of a query is: All orders in 2016, 2015("order.title_number LIKE ?", "%NYN#{year}%") in states (has_many through) NJ, PA, CA, etc with sales_id unlimited ids and counsel_id unlimited counsel_ids.

In a nut shell I am trying to figure out how to create ONE query that takes into account ALL options the user checks. Here is my current query code:

  def Order.query(opt = {})
    results = []
    orders = []

    if !opt["state"].empty?
      opt["state"].each do |value|
        if orders.empty? 
          orders = Order.send("by_state", value)  
        else
          orders << Order.send("by_state", value)
        end
      end
      orders = orders.flatten
    end

    if !opt["year"].empty?
      new_orders = []

      opt["year"].each do |y| 
        new_orders = orders.by_year(y)
        results << new_orders
      end
    end

    if !opt["sales_id"].empty?

    end

    if !opt["counsel_id"].empty?

    end
    if !results.empty?
      results.flatten 
    else
      orders.flatten
    end
  end

Here is the solution I have come up with to allow for unlimited amount of filtering.

 def self.query(opts = {})

    orders = Order.all
    opts.delete_if { |key, value| value.blank? }
    const_query = ""
    state_query = nil
    counsel_query = nil
    sales_query = nil
    year_query = nil
    queries = []

    if opts["by_year"]  
      year_query = opts["by_year"].map do |val|
        " title_number LIKE '%NYN#{val}%' "
      end.join(" or ") 
      queries << year_query
    end     

    if opts["by_sales_rep"]
      sales_query = opts["by_sales_rep"].map do |val|
        " sales_id = '#{val}' "
      end.join(" or ")
      queries << sales_query
    end

    if opts["by_counsel"]
      counsel_query = opts["by_counsel"].map do |val|
        " counsel_id = '#{val}' "
      end.join(" or ")
      queries << counsel_query
    end

    if opts["by_state"]      
      state_query = opts["by_state"].map do |val|
        "states.id = '#{val}'"
      end.join(" or ")
    end

    query_string = queries.join(" AND ")

    if state_query
      @orders = Order.joins(:states).where("#{state_query}")
      @orders = @orders.where(query_string)
    else
      @orders = orders.where("#{query_string}")
    end

    @orders.order("title_number DESC")
  end

Solution

  • What you're looking for a query/filter object, which is a common pattern. I wrote an answer similar to this, but I'll try to extract the important parts.

    First you should move those logic to it's own object. When the search/filter object is initialized it should start with a relation query (Order.all or some base query) and then filter that as you go.

    Here is a super basic example that isn't fleshed out but should get you on the right track. You would call it like so, orders = OrderQuery.call(params).

    # /app/services/order_query.rb
    class OrderQuery
      def call(opts)
        new(opts).results
      end
    
      private
    
      attr_reader :opts, :orders
    
      def new(opts={})
        @opts = opts
        @orders = Order.all  # If using Rails 3 you'll need to use something like
                             # Order.where(1=1) to get a Relation instead of an Array.
      end
    
      def results
        if !opt['state'].empty?
          opt['state'].each do |state|
            @orders = orders.by_state(state)
          end
        end
    
        if !opt['year'].empty?
          opt['year'].each do |year| 
            @orders = orders.by_year(year)
          end
        end
    
        # ... all filtering logic
        # you could also put this in private functions for each
        # type of filter you support.
    
        orders
      end
    end
    

    EDIT: Using OR logic instead of AND logic

    # /app/services/order_query.rb
    class OrderQuery
      def call(opts)
        new(opts).results
      end
    
      private
    
      attr_reader :opts, :orders
    
      def new(opts={})
        @opts = opts
        @orders = Order.all  # If using Rails 3 you'll need to use something like
                             # Order.where(1=1) to get a Relation instead of an Array.
      end
    
      def results
        if !opt['state'].empty?
          @orders = orders.where(state: opt['state'])
        end
    
        if !opt['year'].empty?
          @orders = orders.where(year: opt['year'])
        end
    
        # ... all filtering logic
        # you could also put this in private functions for each
        # type of filter you support.
    
        orders
      end
    end
    

    The above syntax basically filters sayings if state is in this array of states and year is within this array of years.