How can I pass a hash for my where clause? - ruby-on-rails

I am listing products and I want to be able to pass a hash as my where clause so I can do something like:
filter = {}
filter[:category_id] = #category.id
filter[:is_active] = true
#products = Products.where(filter)
Is it possible to do this somehow?
I also need to add something like this in my where clause:
WHERE price > 100
How could I add that to a filter?
The reason I want to do this is because in the UI I will have a set of optional filters, so then I will use if clauses in my controller to set each filter.

You can pass a hash to where exactly like you did:
filter = {
category_id: #category_id,
is_active: true
}
#products = Product.where(filter)
Using a hash only works for equality (e.g. category_id = 123), so you can't put something like price > 100 in there. To add that criteria, just add another where to the chain:
#product = Product.where(filter).where('price > 100')
Or...
#product = Product.where(filter)
if params[:min_price]
#product = #product.where('price > ?', min_price)
end

You could have a bit of fun with scopes: write a scope that's actually a mini predicate builder, sanitizing and pattern-matching strings, and delegating to the standard predicate builder for other scalar types. E.g.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(params) {
params.inject(self) do |rel, (key, value)|
next rel if value.blank?
case value
when String
rel.where arel_table[key].matches '%%%s%%' % sanitize_sql_like(value)
when Range, Numeric, TrueClass, FalseClass
rel.where key => value
else
raise ArgumentError, "unacceptable search type"
end
end
}
end
end
# app/models/product.rb
class Product < ApplicationRecord
include Searchable
then you can
filter = { name: 'cheese', description: 'aged', age: 42.. }
Product.search(filter) #=> SELECT "products".* FROM products WHERE "products"."name" ILIKE '%cheese%' AND "products"."description" ILIKE '%aged%' AND "products"."age" >= 42

Related

Query on Params

I want to get records based on params received. Sometimes I receive 2 params or sometimes 3. I have written a code to get results when all 3 params received but when I receive only 1 param am getting 0 results.
Example
Office.where(state: params[:state], type: params[:type]).where("name like ?","%#{params[:name]}%")
When i get values like
{state: "KA", type: "private", name: "google"}
But when I get the only {name: "google"} I get no records
I have tried with condition
If params[:name].present? && params[:state].present? && params[:type].present?
query
elsif condition
query
end
Let me know how can I solve this or any better way
You can do something like this
In controller
filter_params = params.slice(:state, :type, :name)
Office.filter(filter_params)
In Office model
scope :state, -> (state) { where(state: state) }
scope :type, -> (type) { where(type: type) }
scope :name, -> (name) { where("name LIKE ?", "%#{name}%") }
def self.filter(filter_params)
results = where(nil)
filter_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
PS: It runs a single query irrespective of the number of params!
Hope that helps!
If a parameter is missing, it will probably be blank. If you pass them all in this will result in clauses like type = ''. For example, if only name is passed in you'll get something like...
where name like '%google%' and type = '' and state = ''
You need to strip out the blank fields. There are various ways to do this. Since you have a special case, the name clause, one good way to handle this is to build the query piece by piece.
query = Office.all
query = query.where(state: params[:state]) if params[:state].present?
query = query.where(type: params[:type]) if params[:type].present?
query = query.where("name like ?","%#{params[:name]}%") if params[:name].present?
The query does not execute until you fetch the values from query.
If there's a lot of simple parameters you can make a Hash and remove the pairs with a blank value.
qparams = {
state: params[:state],
type: params[:type]
}.select { |k,v|
v.present?
}
query = Office.where(qparams)
query = query.where("name like ?","%#{params[:name]}%") if params[:name].present?
Or use the handy compact_blank gem.
using CompactBlank
qparams = {
state: params[:state],
type: params[:type]
}.compact_blank
query = Office.where(qparams)
query = query.where("name like ?","%#{params[:name]}%") if params[:name].present?

How to dynamically call scopes with 'OR' clause from an array

I have an array, and I'd like to call scopes with OR clause:
cars = ['bmw', 'audi', 'toyota']
class Car < AR
scope :from_bmw, -> { where(type: 'bmw') }
scope :from_audi, -> { where(type: 'audi') }
scope :from_toyota, -> { where(type: 'toyota') }
end
I'd like to achieve something like this:
Car.from_bmw.or(Car.from_audi).or(Car.from_toyota)
My cars array can change; in case: cars = ['toyota', 'audi'], my method should produce:
Car.from_toyota.or(Car.from_audi)
I have something like the following:
def generate(cars)
scopes = cars.map {|f| "from_#{f} "}
scopes.each do |s|
# HOW TO I ITERATE OVER HERE AND CALL EACH SCOPE???
end
end
I don't want to pass type as an argument to scope, there's a reason behind it.
def generate(cars)
return Car.none if cars.blank?
scopes = cars.map {|f| "from_#{f} "}
scope = Car.send(scopes.shift)
scopes.each do |s|
scope = scope.or(Car.send(s))
end
scope
end
Assuming the given array contains only valid type values, you could simply do that:
class Car
scope :by_type, -> (type) { where(type: type) }
end
types = ['bmw', 'audi', 'toyota']
Car.by_type(types) # => It'll generate a query using IN: SELECT * FROM cars WHERE type IN ('bmw', 'audi', 'toyota')
If you don't want to pass the array as an argument to scope for whatever reason, you could create a hash mapping the array values to valid by_type arguments.
VALID_CAR_TYPES = { volkswagen: ['vw', 'volkswagen'], bmw: ['bmw'], ... }
def sanitize_car_types(types)
types.map do |type|
VALID_CAR_TYPES.find { |k, v| v.include?(type) }.first
end.compact
end
Car.by_type(sanitize_car_types(types))

Too many checks for empty params. How to optimize queries to ActiveRecord in Rails5?

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

Sort Array By Date Attribute, Ordered By Nearest to Current Time

I have an Array of Contact objects...
[#<Contact id: 371, full_name: "Don Hofton", birthday: "2013-11-07">,...]
And I need to order them by birthdays nearest to the current time AND remove objects from the array that have birthdays greater than 4 months away. Here is what I've got so far, but it's not working....
#contacts_with_birthday_data = Contact.where(:user_id => current_user.id).where("birthday IS NOT NULL")
#current_time = Time.now
#contacts_with_birthday_data.each do |c|
c.birthday = c.birthday[0..4]
c.birthday = Date.parse(c.birthday)
end
#contacts_with_birthday_data = #contacts_with_birthday_data.sort! { |a,b| b[:birthday] <=> a[:birthday] }
#contacts_with_birthday_data = #contacts_with_birthday_data.sort! { |a| a.birthday < DateTime.now }
I think you can do this all with one query:
Contact \
.where(:user_id => current_user.id)
.where("birthday > ?", 4.months.ago)
.order("birthday desc")
If 4.months.ago is used in a scope, make sure to wrap it in a lambda or Proc, or it will be calculated when the class is loaded and not on subsequent calls. This has bit me more than once!
Alternatively, in a non-Rails world, you could use the reject and sort_by methods on Enumerable:
contacts = [#your array of contacts]
contacts.reject { |c| c.birthday < 4.months.ago }.sort_by(&:birthday).reverse
If you haven't seen the syntax used in sort_by, that's actually equivalent to sort_by { |c| c.birthday }. That syntax tells Ruby to convert the birthday method to a Proc object, then call the Proc against each instance in your array.

How would I pass sort criteria into a method so that it can be used by sort_by

How would I pass an attribute name to method sort_by, and use it within? For example, say I wanted to create a FrequencyCounter with an array of singleton objects Foo. (It's important that they're singletons because it means that there will be a certain number of identical Foos).
class Foo
attr_accessor :arbitrary_sorter
def initialize arbitrary_sorter
#arbitrary_sorter = arbitrary_sorter
end
end
class FrequencyCounter
def initialize ary
#multiset will create a hash of frequency like {3=>obj, 2->obj, 2=>obj, etc}
#hash = Multiset.new(ary).hash
end
def sort_by params={}
Hash[#hash.sort_by {|k,v| [-1 * v, -1 * k]}]
end
end
And when I create Frequency object:
#fc = FrequencyCounter.new([Foo.get(5), Foo.get(4), Foo.get(5), Foo.get(1)])
I'd like to tell #fc what to order on:
#fc.sort_by(:arbitrary_sorter)
Is this possible?
on any enumerator, you can tell it what to sort by. So, let's take a simple example:
class Animal
attr_accessor :name, :leg_count
def initialize(name, leg_count)
#name = name
#leg_count = leg_count
end
def to_s
#name
end
end
animals = [Animal.new('human', 2), Animal.new('dog', 4), Animal.new('snake', 0)]
=> [human, dog, snake]
# sort by an attribute or method on the model
animals.sort_by{|a| a.leg_count}
=> [snake, human, dog]
Same as:
animals.sort_by(&:leg_count)
=> [snake, human, dog]
You can event do something like:
sorter_lambda = lambda {|animal| animal.name == "human" ? 0 : 999}
animals.sort_by(&sorter_lambda)
=> [human, dog, snake]
The lambda may work best for you with the complicated logic I saw.

Resources