I have these three tables as below
office.rb
has_many :documents
has_one :information
information.rb
belongs_to :office
document.rb
belongs_to :office
I am trying to write a query where as below
documents_controller.rb
def search
#documents = Document.all
#documents.joins(:office).where("offices.name ILIKE ?", "%#{params[:search]}%") OR #documents.joins(:office).joins(:information).where("informations.first_name ILIKE ? OR informations.last_name ILIKE ?", "%#{params[:search]}%", "%#{params[:search]}%")
end
I am trying to achieve the above statement but I am doing something wrong. Please help me fix this query
So, the idea is to retrieve any document where the office's name is the search term or where the information first/last name is the search term, right?
The first step is to create the joins:
Document.joins(office: :information)
The second step is to create the condition:
where("offices.name ILIKE :term OR informations.first_name ILIKE :term OR informations.last_name ILIKE :term", term: "%#{params[:search]}%")
and joining both sentences:
Document.joins(office: :information).where("offices.name ILIKE :term OR informations.first_name ILIKE :term OR informations.last_name ILIKE :term", term: "%#{params[:search]}%")
there are other fancy ways to do the same thing like using or scope but probably will be more complex to understand:
search_term = "%#{params[:search]}%"
base_query = Document.joins(office: :information)
office_scope = base_query.where("offices.name ILIKE :term", search_term)
first_name_scope = base_query.where("informations.first_name ILIKE :term", search_term)
last_name_scope = base_query.where("informations.last_name ILIKE :term", search_term)
office_scope.or(first_name_scope).or(last_name_scope)
Related
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])
I am trying to use a single search field to filter any column following Railscasts 240. The key piece for defining the search function is in the Contact model.
def self.search(search)
if search
joins(:school).where(['name ILIKE ? OR email ILIKE ? OR school.name ILIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
else
all
end
end
This works without the join and school.name. Contact belongs to school and has a school_id column. The exact error is:
PG::AmbiguousColumn: ERROR: column reference "name" is ambiguous
I'm guessing the ambiguous error is because I am trying to search both contact.name and school.name. Looking for a suggestion to allow searching both without adding another search field or needing the user to specify the search type.
Edit:
Good suggestions below to use contact.name to deal with the ambiguity, but that leads to another error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "contact"
LINE 1: ...ON "schools"."id" = "contacts"."school_id" WHERE (contact.na...
^
: SELECT "contacts".* FROM "contacts" INNER JOIN "schools" ON "schools"."id" = "contacts"."school_id" WHERE (contact.name ILIKE '%joseph%' OR email ILIKE '%joseph%' OR school.name ILIKE '%joseph%') ORDER BY name asc LIMIT 50 OFFSET 0
I thought this was due to the inner join moving the entire query into the schools table, but the error persists even if I remove the other two queries and ONLY search on school.name.
The ambiguous errors is caused by PG not knowing which name column the query reffers to - contacts.name or schools.name. You can fix it by changing your query to:
joins(:school).where(['contacts.name ILIKE ? OR email ILIKE ? OR schools.name ILIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
if columns that same names is more then must add alias
try this, i add contacts.name
def self.search(search)
if search
joins(:school).where(['contacts.name ILIKE ? OR email ILIKE ? school.name ILIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
else
all
end
end
In my application I have a customers model with three columns, first_name, middle_name and last_name. I have a method in the model that performs the search:
class Customer < ActiveRecord::Base
belongs_to :user
def self.search(search, user)
if search
.where('first_name LIKE ? OR middle_name LIKE ? OR last_name LIKE ?', "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%")
.where(user: user)
else
where(user: user)
end
end
end
The problem with this search function is that it only allows searching by one of the three columns at a time.
For example, a customer has a first_name of "foo", a middle_name of "bar" and a last_name of "baz". Searching for "foo", "bar", or "baz" individually returns results, but "foo bar" or "bar baz" does not.
What is the best way I can allow searching across all three columns?
You can concat your fields in the database query like
Updated:
.where("concat_ws(' ' , first_name, middle_name, last_name) LIKE ?", "%#{search}%")
This should work for foo, foo bar, or foo bar baz
but not foo baz
If you want to support foo baz as well then
.where("first_name LIKE ? OR middle_name LIKE ?"\
" OR last_name LIKE ? OR concat_ws(' ' , first_name, middle_name, last_name) LIKE ?"\
" OR concat_ws(' ' , first_name, last_name) LIKE ?",
"%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%")
.where("first_name LIKE ? OR middle_name LIKE ? OR last_name LIKE ? or CONCAT(first_name, middle_name, last_name) LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%")
If you use pg, you can try like this
def self.search(query)
where(['phone ilike :query',
'LOWER(name) ilike :query',
'LOWER(email) ilike :query',
'LOWER(address) ilike :query'].join(' OR '), {query: "%#{query}%" })
end
I am working in an app with a basic search form with Heroku, but I can't get my sql query to work properly with PostgreSQL, even though this query worked with MySQL. By the way, I tried to paste the logs from Heroku, but it only says that when you search something it renders 500.html.
Here's my model OrdemDeServico with the search action:
def self.search(search)
if search
joins(:cliente).where("clientes.nome LIKE ? OR veiculo LIKE ? OR placa LIKE ? OR ordem_de_servicos.id = ?", "%#{search}%", "%#{search}%", "%#{search}%", "#{search}")
else
where(nil)
end
end
I just installed PostgreSQL locally, and it returned this error when searching:
`PG::InvalidTextRepresentation: ERROR: invalid input syntax for integer: "Augusto"
LINE 1: ... placa LIKE '%Augusto%' OR ordem_de_servicos.id = 'Augusto')
query:
SELECT "ordem_de_servicos".* FROM "ordem_de_servicos" INNER JOIN "clientes" ON "clientes"."id" = "ordem_de_servicos"."cliente_id" WHERE (clientes.nome LIKE '%Augusto%' OR veiculo LIKE '%Augusto%' OR placa LIKE '%Augusto%' OR ordem_de_servicos.id = 'Augusto') ORDER BY prazo LIMIT 5 OFFSET 0
I finally worked it out a solution. Those who have the same problem here's my code for the model:
def self.search(search)
if search
where("id = ?", search)
joins(:cliente).where("clientes.nome ilike :q or veiculo ilike :q or placa ilike :q", q: "%#{search}%")
else
where(nil)
end
end
How can I refactor this code? Is there a way to split the where clause, the includes and the order functions?
def self.product_search(query, console, genre, sort, order)
if query
#search(query)
if !console.nil? && console != "all" && !genre.nil? && genre != "all"
where("name_en ilike :q AND console_id = :c AND genre_id = :g OR ean ilike :q AND console_id = :c AND genre_id = :g", q: "%#{query}%", c: console, g: genre).includes(:genre, :console, :brand, :images).order("#{sort} #{order}")
elsif !console.nil? && console != "all"
where("name_en ilike :q AND console_id = :c OR ean ilike :q AND console_id = :c", q: "%#{query}%", c: console).includes(:genre, :console, :brand, :images).order("#{sort} #{order}")
elsif !genre.nil? && genre != "all"
where("name_en ilike :q AND genre_id = :g OR ean ilike :q AND genre_id = :g", q: "%#{query}%", g: genre).includes(:genre, :console, :brand, :images).order("#{sort} #{order}")
else
where("name_en ilike :q OR ean ilike :q", q: "%#{query}%").includes(:genre, :console, :brand, :images).order("#{sort} #{order}")
end
end
end
You can build AREL expressions in pieces; they're only executed when they're iterated over or otherwise used. For example, you could do something like this:
def self.product_search(query, console, genre, sort, order)
if query
clause = all # Start with all, filter down.
if !console.nil? && console != "all" && !genre.nil? && genre != "all"
clause = clause.where("name_en ilike :q AND console_id = :c AND genre_id = :g OR ean ilike :q AND console_id = :c AND genre_id = :g", q: "%#{query}%", c: console, g: genre)
elsif !console.nil? && console != "all"
clause = clause.where("name_en ilike :q AND console_id = :c OR ean ilike :q AND console_id = :c", q: "%#{query}%", c: console)
elsif !genre.nil? && genre != "all"
clause = clause.where("name_en ilike :q AND genre_id = :g OR ean ilike :q AND genre_id = :g", q: "%#{query}%", g: genre)
else
clause = clause.where("name_en ilike :q OR ean ilike :q", q: "%#{query}%")
end
clause.includes(:genre, :console, :brand, :images).order("#{sort} #{order}")
end
end
You can keep chaining and assigning until you've built the entire search clause you want. This could be optimized a bunch more, but I think this is sufficient to demonstrate the main point about chaining AREL expressions.
You can also ditch many of those nil checks if you reverse some of the logic and check for console.nil? and genre.nil? first, and then in the else clauses, just check for genre == "all", for example.
It's also possible to define some of these as named scopes on your model (or see this blog post called Named Scopes Are Dead for a better way), to DRY-up some of the code and make it more readable.
My example above is still in need of a lot of work but I think you can assemble some nice code by following that pattern.
this might be taking things to far for you, but i would move that code into another object
# code in Product model
def self.product_search(search_criteria, console, genre, sort, order)
return nil unless search_criteria.present?
ProductSearch.new(search_criteria, genre, sort, order).find
end
# new class to handle Product search
class ProductSearch
def initialize(search_criteria, console, genre, sort, order)
#search_criteria = search_criteria
#console = console
#genre = genre
#sort = sort
#order = order
end
attr_reader :search_criteria, :console, :genre, :sort, :order
def core_query_for_product_search
# WARNING: .order("#{sort} #{order}") is open to sql injection attacks
self.includes(:genre, :console, :brand, :images)
.order("#{sort} #{order}")
.where("name_en ilike :q OR ean ilike :q", q: "%#{search_criteria}%")
end
def with_console?
!console.nil? && console != "all"
end
def with_genre?
!genre.nil? && genre != "all" # you might want genre.present? instead of !genre.nil?
end
def find
query = core_query_for_product_search
query = query.where("genre_id = :g", g: genre) if with_genre?
query = query.where("console_id = :c", c: console) if with_console?
query
end
end
couple of things to note:
1) sql injection in the order clause, rails is good at protecting where clause but not order, see rails 3 activerecord order - what is the proper sql injection work around?
2) This no longer create the exact same sql as your query, but I am guessing the result is the same, rails AREL where chaining will always do AND xxxxx adding OR properly can be more difficult, but in your example code it appeared that OR ean ilike :q is in every one of those queries and no parentheses are used, so i put i in the core, maybe you actually want parentheses and a different result, couldn't understand why AND console_id = :c shows up twice in some of those queries