Suppose I have a method in a controller:
def my_find(is_published, count)
items = Idea.where(published: is_published)
#......
end
Sometimes I want to pass some extra filter arguments
def my_find(is_published, count, some_extra_filter = nil)
items = Idea.where(published: is_published) #.where (some_extra_filter)
#......
end
where some_extra_filter can be lambda or just an plain sql "where" string and it can also be nil or "".
So how do I concatenate .where(published: is_published) with where (some_extra_filter) to get what I need?
This is actually very easy using scopes:
def my_find
#items = Idea.scoped
#items = #items.where(published: is_published) unless is_published.nil?
#items = #items.where(other: other_param) if other_params < 10
# etc, etc
end
Related
I have a where clause being called based off of a json object from a form. The object has a series of boolean values for locales which will be used to find Venues within the given locale.
Is there a better way to write a series of queries than having a long string built off of boolean values? Any help in the right direction would be greatly appreciated. Thanks!
hard_worker.rb
def build_locales
filters = []
filters.push 'locale_north' if #lead.locale_north
filters.push 'locale_south' if #lead.locale_south
filters.push 'locale_east' if #lead.locale_east
filters.push 'locale_west' if #lead.locale_west
return filters
end
def build_string
filters = build_locales
s = ''
filters.each_with_index do |f, i|
s+= "#{f} = true"
s+= " OR " if i < filters.size - 1
end
end
def perform(lead_id)
#venues = Venue.where(receive_all: true).or(Venue.where(build_string))
// ... more code ...
end
where clauses are chainable so you can easily do a lot of querying if you need to with an approach like this.
#venues = Venue.all
#venues = #venues.where(receive_all: true)
filters.each do |filter|
#venues = #venues.or(Venue.where(filter.to_sym: true))
end
# after all of this you can just return #venues
#venues
I am trying to run following query through Rails query interface but unable to translate my logic. The query is
Select f.* from feeds f
Left join feed_items fi on fi.id = f.feedable_id
where
f.feedable_type in ('Homework', 'Datesheet')
and
(
(fi.assignable_type = 'Level' and assignable_id IN (1)) or
(fi.assignable_type = 'Student' and assignable_id IN (1)) or
(fi.assignable_type = 'Section' and assignable_id IN (1))
)
Scenario:
I receive following params hash in my action containing filters which will be added dynamically in my query
{"page"=>"1", "limit"=>"2", "type_filter"=>["Homework", "Datesheet"], "assignable_filter"=>{"Student"=>"[2]", "Section"=>"[1]", "Level"=>"[1]"}}
So far, what I have done is joining the tables and added where clause for type filter but not sure how to dynamically add assignable_filters. Here is my rails code, options are params in following code
def get_feeds(options)
base = Feed.includes(:feed_item)
base = add_type_filters base, options
base = add_assignable_filters base, options
format_response base, options
end
def add_type_filters(base, options)
type_filter = options[:type_filter]
if !type_filter.nil? and type_filter.length > 0
base = base.where('feedable_type IN (?)', options[:type_filter])
end
base
end
def add_assignable_filters(base, options)
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
# code for adding filters combined with or conditions
end
# wrap the or conditions and join them with an and in main where clause
end
base
end
P.S I am using rails 5
There was no straight forward way of building the query dynamically. I had to construct the where string to solve the problem. My current solution is
def get_feeds(options)
params_hash = {}
type_filters = add_type_filters options, params_hash
assignable_filters = add_assignable_filters options, params_hash
where = type_filters
where = where ? "#{where} and (#{assignable_filters})" : assignable_filters
base = Feed.eager_load(:feed_item).where(where, params_hash)
format_response base, options
end
def add_type_filters(options, params_hash)
type_filter = options[:type_filter]
type_filter_sql = nil
if !type_filter.nil? and type_filter.length > 0
type_filter_sql = 'feeds.feedable_type in (:type_filter)'
params_hash[:type_filter] = type_filter
end
type_filter_sql
end
def add_assignable_filters(options, params_hash)
assignable_filter_sql = []
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
assignable_filter_sql.push("(feed_items.assignable_type = '#{key}' and feed_items.assignable_id IN (:#{key}))")
params_hash[key.to_sym] = JSON.parse(value)
end
end
assignable_filter_sql.join(' or ')
end
I'm doing checks for empty parameters before do the query.
There is only 1 check for params[:car_model_id]. I can imagine if I will add more checks for other params, then there will be a mess of if-else statements. It doesn't look nice and I think it can be optimized. But how? Here is the code of controller:
class CarsController < ApplicationController
def search
if params[:car_model_id].empty?
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
else
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
car_model_id: params[:car_model_id],
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
end
if #cars
render json: #cars
else
render json: #cars.errors, status: :unprocessable_entity
end
end
end
The trick would be to remove the blank values, do a little bit of pre-processing (and possibly validation) of the data, and then pass the params to the where clause.
To help with the processing of the date ranges, you can create a method that checks both dates are provided and are converted to a range:
def convert_to_range(start_date, end_date)
if start_date && end_date
price_from = Date.parse(price_from)
price_to = Date.parse(price_to)
price_from..price_to
end
rescue ArgumentError => e
# If you're code reaches here then the user has invalid date and you
# need to work out how to handle this.
end
Then your controller action could look something like this:
# select only the params that are need
car_params = params.slice(:car_model_id, :used, :year_from, :year_to, :price_from, :price_to, :condition)
# do some processing of the data
year_from = car_params.delete(:year_from).presence
year_to = car_params.delete(:year_to).presence
car_params[:price] = convert_to_range(year_from, year_to)
price_from = car_params.delete(:price_from).presence
price_to = car_params.delete(:price_to).presence
car_params[:price] = convert_to_range(price_from, price_to)
# select only params that are present
car_params = car_params.select {|k, v| v.present? }
# search for the cars
#cars = Car.where(car_params)
Also, I'm pretty sure that the used value will automatically get cast to boolean for you when its provided to the where.
Also, #cars is an ActiveRecord::Relation which does not have an errors method. Perhaps you mean to give different results based on whether there are any cars returned?
E.g: #cars.any? (or #cars.load.any? if you don't want to execute two queries to fetch the cars and check if cars exist)
Edit:
As mentioned by mu is too short you can also clean up your code by chaining where conditions and scopes. Scopes help to move functionality out of the controller and into the model which increases re-usability of functionality.
E.g.
class Car > ActiveRecord::Base
scope :year_between, ->(from, to) { where(year: from..to) }
scope :price_between, ->(from, to) { where(price: from..to) }
scope :used, ->(value = true) { where(used: used) }
end
Then in your controller:
# initial condition is all cars
cars = Cars.all
# refine results with params provided by user
cars = cars.where(car_model_id: params[:car_model_id]) if params[:car_model_id].present?
cars = cars.year_between(params[:year_from], params[:year_to])
cars = cars.price_between(params[:price_from], params[:price_to])
cars = cars.used(params[:used])
cars = cars.where(condition: params[:condition]) if params[:condition].present?
#cars = cars
I'm trying to filter the results that are returned from my index view based on optional params. My code is working for the first param, sinceDate. But for the second param, searchQeury, nothing is filtered out.
_controller.rb
def index
since = params[:sinceDate]
query = params[:searchQuery]
#articles = Comfy::Cms::Page.published.all
if since
#articles = #articles.reject{ |a| a[:created_at] < Date.parse(since) }
end
if query
#article = #articles.select{ |a| a[:label].match(/#{query}/i) }
end
end
Is it possible that the problem is a typo?
In the line after "if query", it should be perhaps #articles instead of #article.
I am working on an events application where i want to filter events depending on the 3 parameters location or starts_at or ends_at in the query string. There can be any one, two or all the parameters in the query string. In i use if-else statement i need to make 6 cases which will make my code clumsy. Rather i am thinking to implement something this way:
class EventsController < ApplicationController
def index
unless params.empty?
unless params[:location].nil?
#events = Event.where("location = ?", params[:location])
end
unless params[:starts_at].nil?
unless #events.empty?
#events = #events.where("start_date = ?", params[:start_date])
else
#events = Event.where("Date(starts_at) = Date(?)", params[:starts_at])
end
end
unless params[:ends_at].nil?
unless #events.empty?
#events = #events.where("end_date = ?", params[:end_date])
else
#events = Event.where("Date(ends_at) = Date(?)", params[:ends_at])
end
end
end
end
end
But this code doesnt work since where query doen not work on an array. Can someone suggest me some solution for this..
You should be able to pass your params hash directly to where, and it will form the correct SQL based on the keys and values of that hash:
Event.where(params)
An example in the console:
1.9.3p194 :001 > puts Example.where(:location => 'here', :started_at => '2012-08-13').to_sql
SELECT "examples".* FROM "examples" WHERE "examples"."location" = 'here' AND "examples"."started_at" = '2012-08-13'
Try Following
def index
unless params.empty?
where_array, arr = [], []
if params[:location]
where_array << "location = ?"
arr << params[:location]
end
if params[:starts_at]
where_array << "start_date = ?"
arr << params[:starts_at]
end
if params[:ends_at]
where_array << "end_date = ?"
arr << params[:ends_at]
end
#events = arr.blank? ? [] : Event.where([where_array.join(" AND "), *arr])
end
end