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
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
.