Dynamic "OR" conditions in Rails 3 - ruby-on-rails

I am working on a carpool application where people can search for lifts. They should be able to select the city from which they would liked to be picked up and choose a radius which will then add the cities in range to the query. However the way it is so far is that i can only chain a bunch of "AND" conditions together where it would be right to say "WHERE start_city = city_from OR start_city = a_city_in_range OR start_city = another_city_in_range"
Does anyone know how to achive this? Thanks very much in advance.
class Search < ActiveRecord::Base
def find_lifts
scope = Lift.where('city_from_id = ?', self.city_from)
#returns id of cities which are in range of given radius
#cities_in_range_from = City.location_ids_in_range(self.city_from, self.radius_from)
#adds where condition based on cities in range
for city in #cities_in_range_from
scope = scope.where('city_from_id = ?', city)
#something like scope.or('city_from_id = ?', city) would be nice..
end
end

You can use "IN" operator instead of "=" without using SQL syntax
With Arel (used by Rails 3.0) you can do that this way
class Search < ActiveRecord::Base
def find_lifts
#returns id of cities which are in range of given radius
#cities_in_range_from = City.location_ids_in_range(self.city_from, self.radius_from)
scope = Lift.where{ :city_from_id => [self.city_from] + #cities_in_range_from })
# ... and so on
end
end

Related

ActiveRecord Rails: Get records where a field is unique/distinct

I have a project Ruby 2.4.0 and Rails 5.0.1 and a model with the following:
class Hospital < ApplicationRecord
validates_presence_of :name
validates_presence_of :state
validates_presence_of :unit
validates_presence_of :site
def self.get_hospitals
hospitals = order(:name).all
grouped = hospitals.group_by(&:state)
end
end
I then use that "grouped" hash to populate a drop down box in which I display h.name + ' (' + h.unit.to_s + ')'.
My issue is that there can be duplicates in that list and if there is then I only want one of them.
How do I query the records and get returned the whole records (not pluck or select where only a single field is returned) whereby name is unique?
Something like:
hospitals = order(:name).distinct
I don't know how you can do it with SQL but it's possible with the method uniq of Array:
def self.get_hospitals
hospitals = order(:name).all
unique = hospitals.uniq(&:name)
grouped = unique.group_by(&:state)
end
Here's one way to get all objects by a unique attribute:
Hospital.select('distinct on (name) *')
I guess this will help you
hospitals = order(:name).group(:name)
You can try distinct
distinct.order(name: :desc)
If you want group by then you can also try group
distinct.group(:id, :state).order(name: :desc)
will give you instead of hospitals.group_by(&:state)
The query will return something like below
SELECT DISTINCT "hospitals".* FROM "hospitals" GROUP BY "hospitals"."id", "hospitals"."state" ORDER BY "hospitals"."name" DESC
I couldn't get it working at the SQL level so I implemented a '.uniq' solution on the Array that I pass to my drop down list.
def self.get_hospitals
hospitals = order(:name).all
grouped = hospitals.group_by(&:state)
output = grouped.collect { |state, hospitals| [state, hospitals.collect { |h| [h.name, h.id] }] }
output.each do |key, value|
value.uniq!
end
output.sort { |a, b| a[0] <=> b[0] }
end

How to use find_by_sql properly?

I have the following model
class Backup < ActiveRecord::Base
belongs_to :component
belongs_to :backup_medium
def self.search(value)
join_tables = "backups, components, backup_media"
joins = "backups.backup_medium_id = backup_media.id and components.id = backups.component_id"
c = Backup.find_by_sql "select * from #{join_tables} where components.name like '%#{value}%' and #{joins}"
b = Backup.find_by_sql "select * from #{join_tables} where backup_media.name like '%#{value}%' and #{joins}"
c.count > 0 ? c : b
end
end
In pry, when I run Backup.all.class, I get
=> Backup::ActiveRecord_Relation
but when I run Backup.search('xxx').class, I get
=> Array
Since the search should return a subset of all, I think I need to return an Active Record_Relation. What am I missing?
From the documentation:
Executes a custom SQL query against your database and returns all the
results. The results will be returned as an array with columns
requested encapsulated as attributes of the model you call this method
from. If you call Product.find_by_sql then the results will be
returned in a Product object with the attributes you specified in the
SQL query.
So you will get an array of Backup instances.
Note that you probably should not do it this way. Using string interpolation in a query opens you up to SQL injection attacks and gains you nothing. Also, you can get quite a bit more flexibility using ActiveRecord scopes for this.
def self.my_includes
includes(:components, :backup_media)
end
def self.by_component_name(name)
media_includes.where("components.name like ?", "'%#{name}%'")
end
def self.by_media_name(name)
media_includes.where("backup_media.name like ?", "'%#{value}%'")
end
def self.search(name)
by_component(name).any? ? by_component_name : by_media_name
end
You can then call
Backup.search(name)
as well as
Backup.by_component_name(name)
or
Backup.by_media_name(name)
find_by_sql returns an array of objects, not a Relation. If you want to return relation for consistency try to rewrite your search to use ActiveRecord api:
def self.search(value)
query = Backup.includes(:component, :backup_medium)
by_component_name = query.where("components.name like ?", "'%#{value}%'")
by_media_name = query.where("backup_media.name like ?", "'%#{value}%'")
by_component_name.any? ? by_component_name : by_media_name
end
or, if you still want to use sql, you can try to fetch record ids and then make a second query:
def self.search(value)
# ...
c = Backup.find_by_sql "select id from #{join_tables} where components.name like '%#{value}%' and #{joins}"
b = Backup.find_by_sql "select id from #{join_tables} where backup_media.name like '%#{value}%' and #{joins}"
ids = c.count > 0 ? c : b
Backup.where(id: ids)
end
So I am unable to get the syntax right for the media_includes, but inspired by your solution I have succeeded by using joins.
I created a small demo project which just shows the code related to search. You can take a look at https://github.com/pamh09/rails-search-demo. If you want to collaborate on a solution, I think this would be more efficient than trying to paste all the code here. That said, I do have a working solution if you'd rather not bother. But I would like to see what the right syntax is.
Below is the model code. It's very possible that I just have some kind of syntactic mismatch since I am not very familiar with how rails does its database magic (obviously).
class Backup < ApplicationRecord
belongs_to :component
belongs_to :backup_medium
#---- code below does not work ---
# in pry
# pry(Backup):1> by_media('bak').any?
# (0.0ms) SELECT COUNT(*) FROM "backups" WHERE (backup_media = 'bak')
# ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: backup_media.name: SELECT COUNT(*) FROM "backups" WHERE (backup_media.name = 'bak')
def self.my_includes
includes(:component, :backup_medium)
end
def self.by_component(name)
my_includes.where("components.name = ?", name)
end
def self.by_media(name)
my_includes.where("backup_media.name = ?", name)
end
def self.search_by(name)
by_component(name).any? ? by_component_name : by_media_name
end
# ----- code below works ... call search('string') -----
# I was unable to get the like query to work without using #{name}
def self.by_component_like(name)
# Note: joins (singular).where (plural.column ...)
joins(:component).where("components.name like '%#{name}%'")
end
def self.by_media_like(name)
joins(:backup_medium).where("backup_media.name like '%#{name}%'")
end
def self.search(name)
by_component_like(name).any? ? by_component_like(name) : by_media_like(name)
end
end
And, as noted in the code. I could not figure you how to use the ? with LIKE as the query would come in as LIKE '%'xxx'%' instead of '%xxx%'.

Specific search implementation

So I'm trying to improve the search feature for my app
My model relationships/associations are like so (many>one, one=one):
Clients < Projects < Activities = Assignments = Users
Assignments < Tasks
Tasks table has only a foreign key to assignments.
Search params look something like this:
params[:search]==User: 'user_handle', Client: 'client_name', Project: 'project_name', Activity: 'activity_name'
So I need to porbably search Clients.where().tasks, Projects.where().tasks and so on.
Then I need to somehow concatenate those queries and get rid of all the duplicate results. How to do that in practice however, I have no clue.
I've been hitting my head against a brick wall with this and internet searches didn't really help... so any help is greatly apreciated. Its probably a simple solution too...
I am on rails 4.2.5 sqlite for dev pg for production
A few things I would change/recommend based on the code in your own answer:
Move the search queries into scopes on each model class
Prefer AREL over raw SQL when composing queries (here's a quick
guide)
Enhance rails to use some sort of or when querying Models
The changes I suggest will enable you to do something like this:
search = search_params
tasks = Tasks.all
tasks = tasks.or.user_handle_matches(handle) if (handle = search[:user].presence)
tasks = tasks.or.client_name_matches(name) if (name = search[:client].presence)
tasks = tasks.or.project_name_matches(name) if (name = search[:project].presence)
tasks = tasks.or.activity_name_matches(name) if (name = search[:activity].presence)
#tasks = tasks.uniq
First, convert each of your queries to a scope on your models. This enables you to reuse your scopes later:
class User
scope :handle_matches, ->(handle) {
where(arel_table[:handle].matches("%#{handle}%"))
}
end
class Client
scope :name_matches, ->(name) {
where(arel_table[:name].matches("%#{name}%"))
}
end
class Project
scope :name_matches, ->(name) {
where(arel_table[:name].matches("%#{name}%"))
}
end
class Activity
scope :name_matches, ->(name) {
where(arel_table[:name].matches("%#{name}%"))
}
end
You can then use these scopes on your Task model to allow for better searching capabilities. For each of the scopes on Task we are doing an join (inner join) on a relationship and using the scope to limit the results of the join:
class Task
belongs_to :assignment
has_one :user, :through => :assignment
has_one :activity, :through => :assignment
has_one :project, :through => :activity
scope :user_handle_matches, ->(handle) {
joins(:user).merge( User.handle_matches(handle) )
}
scope :client_name_matches, ->(name) {
joins(:client).merge( Client.name_matches(name) )
}
scope :activity_name_matches, ->(name) {
joins(:activity).merge( Activity.name_matches(name) )
}
scope :project_name_matches, ->(name) {
joins(:project).merge( Project.name_matches(name) )
}
end
The final problem to solve is oring the results. Rails 4 and below don't really allow this out of the box but there are gems and code out there to allow this functionality.
I often include the code in this GitHub gist in an initializer to allow oring of scopes. The code allows you to do things like Person.where(name: 'John').or.where(name: 'Jane').
Many other options are discussed in this SO question.
If you don't want include random code and gems, another option is to pass an array of ids into the where clause. This generates a query similar to SELECT * FROM tasks WHERE id IN (1, 4, 5, ...):
tasks = []
tasks << Tasks.user_handle_matches(handle) if (handle = search[:user].presence)
tasks << tasks.or.client_name_matches(name) if (name = search[:client].presence)
tasks << tasks.or.project_name_matches(name) if (name = search[:project].presence)
tasks << tasks.or.activity_name_matches(name) if (name = search[:activity].presence)
# get the matching id's for each query defined above
# this is the catch, each call to `pluck` is another hit of the db
task_ids = tasks.collect {|query| query.pluck(:id) }
tasks_ids.uniq!
#tasks = Tasks.where(id: tasks_ids)
So I solved it, it is supper sloppy however.
first I wrote a method
def add_res(ar_obj)
ar_obj.each do |o|
res += o.tasks
end
return res
end
then I wrote my search logic like so
if !search_params[:user].empty?
query = add_res(User.where('handle LIKE ?', "%#{search_params[:user]}%"))
#tasks.nil? ? #tasks=query : #tasks=#tasks&query
end
if !search_params[:client].empty?
query = add_res(Client.where('name LIKE ?', "%#{search_params[:client]}%"))
#tasks.nil? ? #tasks=query : #tasks=#tasks&query
end
if !search_params[:project].empty?
query = add_res(Project.where('name LIKE ?', "%#{search_params[:project]}%"))
#tasks.nil? ? #tasks=query : #tasks=#tasks&query
end
if !search_params[:activity].empty?
query = add_res(Activity.where('name LIKE ?', "%#{search_params[:activity]}%"))
#tasks.nil? ? #tasks=query : #tasks=#tasks&query
end
if #tasks.nil?
#tasks=Task.all
end
#tasks=#tasks.uniq
If someone can provide a better answer I would be forever greatful

Dynamic query with multiple OR conditions

On an ActiveRecord model, I'm trying to dynamically create a query that has multiple OR conditions. ie:
SELECT * from articles
WHERE (name LIKE '%a%' OR desc LIKE '%a%' OR
name LIKE '%b%' OR desc LIKE '%b%' OR
name LIKE '%c%' OR desc LIKE '%c%')
I think I'm on the right track using arel, but I can't work out how to start off the query.
class Article < ActiveRecord::Base
attr_accessor :title, :text
def self.search(terms)
terms = *terms
t = self.arel_table
query = terms.reduce(???) do |query, word|
search_term = "%#{word}%"
query.or(t[:title].matches(search_term).or(t[:text].matches(search_term)).expr).expr
end
where(query)
end
end
I originally got the idea from this answer, but the original query is a string obviously and not something I can chuck .or onto.
What do I need to replace ??? in the reduce method to make this work, or do I need to take a completely different path (as I suspect)?
This is what I did to get it working:
class Article < ActiveRecord::Base
attr_accessor :title, :text
def self.search(terms)
terms = *terms
t = self.arel_table
# generate array of conditions
query = terms.collect do |word|
search_term = "%#{word}%"
t[:title].matches(search_term).or(t[:text].matches(search_term)).expr
end
# combine conditions
query = query.reduce {|query, condition| query.or(condition).expr }
where(query)
end
end

Returning a list with ActiveRecord query interface

My Program class has_many Patients
and Patient class has a field like salary
And the json I pass to JBuilder with respond_to :json , respond_with(#org) is like this:
#org = Org.includes(programs: [{hospitals: :nurses}, :patients]).find(params[:id])
Well now if my database has 200 programs that meed the org_id = params[:id] condition it will return them all. BUT this is Not what I want. I want to tell it to return "5" programs and those "five" programs that the "salary" field of their :'patients" table is the highest.
How can I implement this limitation in active record query?
Something like
Program.includes([{hospitals: :nurses}, :patients]).where("organization_id = ?", params[:id]).order("patients.salary desc").limit(5)
Or you can do:
#program_ids = Program.select("id").includes([{hospitals: :nurses}, :patients]).where("organization_id = ?", params[:id]).order("patients.salary desc").limit(5)
#org = Org.includes(programs: [{hospitals: :nurses}, :patients]).where("programs.id = ?", program_ids.collect(&:id)).find(params[:id])
You can also refactor with a subquery in one step (still 2 queries), by checking this question:
subqueries in activerecord
I'm not sure if you can get that information in just one single query

Resources