Filtering items by multiple fields - ruby-on-rails

Filtering items by title, created_at dates, but one of them would be empty and it raises an error, how could I handle that?
where("country_translations.title ILIKE ? AND country_translations.created_at > ? AND country_translations.created_at < ?", "%#{search[:title]}%", search[:created_at_gt], search[:created_at_lt])

You can do something like this:
YourModel.where(filter_by_translations_title).where(filter_by_translations_created)
def filter_by_translations_title
['country_translations.title ILIKE ?', search[:title]] if search[:title].present?
end
#...add rest of methods here
Chaining #where will join all queries via AND. This way will allow you add as many sub quesries as you want, and control their behavior.

You can chain your where clauses quite easily.
#results = Model.all
#results = #results.where('country_translations.title ILIKE ?', "%#{search[:title]}%") if search[:title].present?
If you're using Postgres you can also use a regex instead of ILIKE to get rid of this %#{}% stuff.
#results = #results.where('country_translations.title ~* ?', search[:title]) if search[:title].present?
and so on for your other fields.

It really depends on how you want to handle that.
First of all, I will decompose the query into multiple wheres, that default to an AND operation. This is for readability:
Model.where("country_translations.title ILIKE ?", "%#{search[:title]}%")
.where("country_translations.created_at > ?", search[:created_at_gt])
.where("country_translations.created_at < ?", search[:created_at_lt])
You could either pass default values using the || operator, like this:
Model.where("country_translations.title ILIKE ?", "%#{search[:title] || ''}%")
.where("country_translations.created_at > ?", search[:created_at_gt] || Time.now)
.where("country_translations.created_at < ?", search[:created_at_lt] || Time.now)
or you can split this into three filters that have to be applied only when needed:
query = Model.all
query = query.where("country_translations.title ILIKE ?", "%#{search[:title]}%") if search[:title]
query = query.where("country_translations.created_at > ?", search[:created_at_gt]) if search[:created_at_gt]
query = query.where("country_translations.created_at < ?", search[:created_at_lt]) if search[:created_at_lt]
# query now is filtered only with present filters.

you can always use scopes in such case, they come handy almost everywhere
scope :filter_by_title, -> (title) { where('title ILIKE ?', "%#{title}%") if title.present? }
scope :filter_by_created_at_lt, -> (date) { where('created_at < ?', date) if date.present? }
scope :filter_by_created_at_gt, -> (date) { where('created_at > ?', date) if date.present? }
Then you can restructure the query as
Model.filter_by_title(search[:title])
.filter_by_created_at_lt(search[:created_at_lt])
.filter_by_created_at_gt(search[:created_at_gt])

Related

handle ruby send nil

scope :
class Car < ApplicationRecord
scope :sold_between, -> (start_date, end_date,exclude_used_cars=true){
_used = exclude_used_cars ? "exclude_used_cars" : ""
Car.where("start_date <= ?", start_date ).and("end_date <= ?", end_date ).send(_used)
}
scope :exclude_used_cars, -> {
where.not(state: :used)
}
Problem:
stuck with .send(_used) I need to pass some valid symbols, but actually I have nil value exclude_used_cars when it is false.
Any better way solving this. Thanks
This can be solved by using a normal if and taking advantage of the chaining nature of queries.
Note that your where clause isn't quite right. Values need to be passed in using placeholders. While there is a .or there is no .and. Additional .where calls will go together with and.
Also note that I've avoided hard coding the Car class name in the scope. This ensures it will work with subclasses.
scope :sold_between, -> (start_date, end_date,exclude_used_cars=true){
query = where(
"start_date <= :start_date and end_date >= :end_date",
{ start_date: start_date, end_date: end_date }
)
if exclude_used_cars
query = query.exclude_used_cars
end
query
}
Because you can chain queries and scopes like this consider whether there's a need for a special exclude_used_cars parameter, especially one that defaults to true. The user of sold_between can as easily add the scope themselves. This is simpler and more explicit.
scope :sold_between, -> (start_date, end_date) {
where(
"start_date <= :start_date and end_date >= :end_date",
{ start_date: start_date, end_date: end_date }
)
}
# All cars
Cars.sold_between(start, end)
# Without used cars
Cars.sold_between(start, end).exclude_used_cars

How to reduce number of queries in this case on Rails

I need to do a bunch of queries.
In this case I think I query a result by many times,
Not completed in one query.
How to make my search results can be done by one query ?
q = WeatherLog.nearby(100, longitude, latitude)
if start_time and end_time
#weather_logs = q.where(datetime: start_time..end_time)
elsif start_time
#weather_logs = q.where("datetime > ?", start_time)
elsif end_time
#weather_logs = q.where("datetime < ?", end_time)
end
#weather_logs = #weather_logs.order(datetime: :asc).first(2000)
The first thing to realize is that ActiveRecord does not execute a query until it aboultely has to (lazy loading). While there are a number of lines of code building up the query, the query is only executed on methods like .all, .each, .first etc. So from a performance standpoint your code is ok as your only executing one query to the database and not many.
However you can tweak the code to make it more human readable and maintainable:
class WeatherLog < ActiveRecord::Base
# ...
class << self
def between_times(times)
after_time(times[:start_time]).before_time(times[:end_time])
end
def after_time(time)
return self.all if time.nil?
where('datetime > ?', time)
end
def before_time(time)
return self.all if time.nil?
where('datetime < ?', time)
end
end
end
Using self.all effectively skips the query condition while still enabling query chaining. This makes it possible to remove of all the if/else logic. Then you can chain the queries (or create a helper method within WeatherLog):
WeatherLog.nearby(100, longitude, latitude).between_times(start_time: start_time, end_time: end_time)

WHERE statement in Rails?

Just wondering what the correct way of structuring statements like this is?
#products = Product.where('release_date <= ?', Date.today AND 'physical_status' == 'active')
Thanks!
What you're looking for is this:
#products = Product.where('release_date <= ? AND physical_status = ?', Date.today, 'active')
You can take a look at this guide: http://guides.rubyonrails.org/active_record_querying.html

Search multiple columns - Rails

I am currently writing a search method for my rails applications, and at the moment it works fine. I have the following in my game.rb:
def self.search(search)
if search
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "#{search}", "#{search}"])
else
find(:all)
end
end
Now that searches fine, but my problem is that if there is a record in game_name that has the word 'playstation' in it, it will finish the search there. It only returns that record, rather than all games that have 'playstation' stored in console. Now I understand this is because I have 'OR' in my conditions, but I don't know an alternative. 'AND' requires all the conditions to match or none return at all. What is an alternative I can use to AND and OR? Help would be much appreciated.
If there is a solution that has separate search boxes and entries, then that would be fine, I don't necessarily require the search to find it all based on one search form.
If I understand your question correctly, your SQL looks good to me for what you are trying to do. An OR clause will return all records that match in column1, column2, or column3. It doesn't stop at the first match. I do see an issue with your parameters in that the first you are using LIKE with % but in the second two you aren't, maybe that is where your issue is coming from.
Should this be your find (% around second and third search)?
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
or better use DRY version (above will not work for Rails 4.2+):
Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: "%#{search}%")
What if you have 15 columns to search then you will repeat key 15 times. Instead of repeating key 15 times in query you can write like this:
key = "%#{search}%"
#items = Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: key).order(:name)
It will give you same result.
Thanks
I think this is a little bit of a cleaner solution. This allows you to add/remove columns more easily.
key = "%#{search}%"
columns = %w{game_name genre console}
#items = Item.where(
columns
.map {|c| "#{c} like :search" }
.join(' OR '),
search: key
)
A more generic solution for searching in all fields of the model would be like this
def search_in_all_fields model, text
model.where(
model.column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
end
Or better as a scope in the model itself
class Model < ActiveRecord::Base
scope :search_in_all_fields, ->(text){
where(
column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
}
end
You would just need to call it like this
Model.search_in_all_fields "test"
Before you start.., no, sql injection would probably not work here but still better and shorter
class Model < ActiveRecord::Base
scope :search_all_fields, ->(text){
where("#{column_names.join(' || ')} like ?", "%#{text}%")
}
end
I think this is a more efficient solution if you want to search an array of columns as I do.
First and most importantly you can add a private function to your model that creates a query template:
def self.multiple_columns_like_query(array)
array.reduce('') { |memo, x| #
unless memo == '' #
memo += ' or ' # This is the
end #
memo += "#{x} like :q" # core part
} #
end
Than you can use the function in your search function:
def self.search(query)
if fields = self.searched_fields && query
where multiple_like_query(fields), q: "%#{query}%"
end
end
Here you should also define self.searched_fields as an array of field names.

Rails 3: How to merge queries or scopes for complex query?

I'm building an events app that is very simple, it has a title and start_date and end_date. I would like to filter my query by mixing some of the values, like: if the start_date has passed but the end_date has not, the event is active and should be displayed. If both dates have passed, it should be omitted, too. I think that scopes is the aswer, but I only was able to filter the records within the view using some methods shown below.
I really would like to filter the query that is passed to the controller (#events). I want to show all events that are active, have a future start_date, or a past start_date but are still in progress (Today's date is in range between start_date and end_date)
EDITED
I have made some scopes which return each part of the query. Chaining them actually substracts the results instead of merging them. So i have used this code and actually works do I do not know how solid or DRY this is. Looks kind of ugly to me... is this a decent way to merge queries in rails 3?
scope :active, where("active = ?", true)
scope :not_over_or_in_progress, lambda { where("start_date < ? AND end_date > ? OR end_date IS NULL AND start_date > ? OR end_date IS NOT NULL AND start_date > ?", Date.today, Date.today, Date.today, Date.today) }
scope :valid, not_over_or_in_progress.active.order("start_date DESC")
Try using scopes:
class Event < AR::Base
scope :active, lambda { |date| where("start_date < ? AND end_date > ?", date) }
scope :future, lambda { |date| where("end_date < ?", date }
...
end
# Console
> #active_events = Event.active(Date.today)
> #future_events = Event.future(Date.today)
See http://guides.rubyonrails.org/active_record_querying.html

Resources