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
Related
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)
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
I am building a rails app where users can post, must like a form. I want users to be able to search through the posts with a text field, and then all records will be returned where the post content is similar to the user query. For example, a user enters:
'albert einstein atomic bomb'
Then, I want a to run a query where every word is checked, along with the query itself. Something like:
query_result = Hash.new
query_result << Post.where('content ILIKE ?', "%albert%")
query_result << Post.where('content ILIKE ?', "%einstein%")
query_result << Post.where('content ILIKE ?', "%atomic%")
query_result << Post.where('content ILIKE ?', "%bomb%")
query_result << Post.where('content ILIKE ?', "%albert einstein atomic bomb%")
This will not work of course, but I hope you get the idea. Any and all input would be appreciated.
You can use sunspot gem for stuff like this. Once it's setup you can do searches like so
# Posts with the exact phrase "great pizza"
Post.search do
fulltext '"great pizza"'
end
# One word can appear between the words in the phrase, so "great big pizza"
# also matches, in addition to "great pizza"
Post.search do
fulltext '"great pizza"' do
query_phrase_slop 1
end
end
and more. See the info in the link.
What about something like this?
query = 'albert einstein atomic bomb'
sub_queries = query.split("\n") << query
query_results = []
sub_queries.each { |q| query_results << Post.where('content ILIKE ?', "%#{q}%") }
You probably need to flatten the query_results array and uniq to remove duplicates.
This query won't return any records, when hidden_episodes_ids is empty.
:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ? AND episodes.id NOT IN (?)", #show_ids, #start_day, #end_day, hidden_episodes_ids]
If it's empty, the SQL will look like NOT IN (null)
So my solution is:
if hidden_episodes_ids.any?
*mode code*:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ? AND episodes.id NOT IN (?)", #show_ids, #start_day, #end_day, hidden_episodes_ids]
else
*mode code*:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ?", #show_ids, #start_day, #end_day]
end
But it is rather ugly (My real query is actually 5 lines, with joins and selects etc..)
Is there a way to use a single query and avoid the NOT IN (null)?
PS: These are old queries migrated into Rails 3, hence the :conditions
You should just use the where method instead as that'll help clean all of this up. You just chain it together:
scope = Thing.where(:episodes => { :show_id => #show_ids })
scope = scope.where('air_date BETWEEN ? AND ?', #start_day, #end_day)
if (hidden_episode_ids.any?)
scope = scope.where('episodes.id NOT IN (?)', hidden_episode_ids)
end
Being able to conditionally modify the scope avoids a lot of duplication.
I'm using the Rails3 beta, will_paginate gem, and the geokit gem & plugin.
As the geokit-rails plugin, doesn't seem to support Rails3 scopes (including the :origin symbol is the issue), I need to use the .find syntax.
In lieu of scopes, I need to combine two sets of criteria in array format:
I have a default condition:
conditions = ["invoices.cancelled = ? AND invoices.paid = ?", false, false]
I may need to add one of the following conditions to the default condition, depending on a UI selection:
#aged 0
lambda {["created_at IS NULL OR created_at < ?", Date.today + 30.days]}
#aged 30
lambda {["created_at >= ? AND created_at < ?", Date.today + 30.days, Date.today + 60.days]}
#aged > 90
lamdba {["created_at >= ?", Date.today + 90.days]}
The resulting query resembles:
#invoices = Invoice.find(
:all,
:conditions => conditions,
:origin => ll #current_user's lat/lng pair
).paginate(:per_page => #per_page, :page => params[:page])
Questions:
Is there an easy way to combine these two arrays of conditions (if I've worded that correctly)
While it isn't contributing to the problem, is there a DRYer way to create these aging buckets?
Is there a way to use Rails3 scopes with the geokit-rails plugin that will work?
Thanks for your time.
Try this:
ca = [["invoices.cancelled = ? AND invoices.paid = ?", false, false]]
ca << ["created_at IS NULL OR created_at < ?",
Date.today + 30.days] if aged == 0
ca << ["created_at >= ? AND created_at < ?",
Date.today + 30.days, Date.today + 60.days] if aged == 30
ca << ["created_at >= ?", Date.today + 90.days] if aged > 30
condition = [ca.map{|c| c[0] }.join(" AND "), *ca.map{|c| c[1..-1] }.flatten]
Edit Approach 2
Monkey patch the Array class. Create a file called monkey_patch.rb in config/initializers directory.
class Array
def where(*args)
sql = args[0]
unless (sql.is_a?(String) and sql.present?)
return self
end
self[0] = self[0].present? ? " #{self[0]} AND #{sql} " : sql
self.concat(args[1..-1])
end
end
Now you can do this:
cond = []
cond.where("id = ?", params[id]) if params[id].present?
cond.where("state IN (?)", states) unless states.empty?
User.all(:conditions => cond)
I think a better way is to use Anonymous scopes.
Check it out here:
http://railscasts.com/episodes/112-anonymous-scopes