Would like to build rails active record query with multiple optional where conditions.
Example:
I have a patient search form that able to search by id, name and email address. The pseudo code would be as below:
where_sql = ""
where_sql = {'name = ?", params[:name]} if params[:name]
where_sql = {'id = ?", params[:id]} if params[:id]
where_sql = {'email = ?", params[:email]} if params[:email]
Patient.where(where_sql)
How do I build following queries without worrying about sql injection.
If you use questionmark "?" placeholders or hashes ActiveRecord automatically escapes the values for you. See injection countermeasures in rails guides http://guides.rubyonrails.org/security.html#sql-injection
This might be a nice use case for the ransack gem (MetaWhere rewrite) https://github.com/ernie/ransack
If you are using only equal conditions, you can do it like:
conditions = {}
conditions[:name] = params[:name] if params[:name]
conditions[:id] = params[:id] if params[:id]
conditions[:email] = params[:email] if params[:email]
Patient.where(conditions)
Also, take a look to a great searchlogic gem.
Related
I have a payment_request model and a payment_detail model. In the payment_request index I need to be able to search by first and last name which are stored in the payment_details table. I am newish to writing SQL and could use some help. I have what I believe to be the correct query below, but am not sure how to write that in my Rails controller so I can search by name.
SELECT first_name, last_name
FROM payment_details
LEFT OUTER JOIN payment_requests
ON payment_requests.id = payment_details.payment_request_id;
If you're using ActiveRecord models, you can skip all that and build that query with the ActiveRecord Querying Interface.
#payment_requests = PaymentRequest.joins(:payment_detail).where(payment_detail: {first_name: params[:first_name], last_name: params[:last_name]})
If you intent to show payment_details data on that index page, you should consider including that information in that query, so you avoid n+1 queries.
#payment_requests = PaymentRequest.includes(:payment_detail).where(payment_detail: { first_name: params[:first_name], last_name: params[:last_name]})
Note: You've got to have a complete match to use the above, so it may not be what you want.
I'd also recommend you use the Ransack gem to build complex queries. It would go something like this:
PaymentRequest.ransack(params[:q])
and in your views:
<%= f.search_field :payment_detail_first_name_or_payment_detail_last_name_cont %>
That would allow you to use just one field to query both columns.
You can do the following:
term_to_find = params[:search]
columns_to_search = %w( payment_details.first_name payment_details.last_name )
sql_conditions = []
columns_to_search.map |column_name|
sql_conditions.push("#{column_name} ILIKE :term_to_find")
end
PaymentRequest.includes(:payment_details)
.where(sql_conditions.join(' OR '), term_to_find: "%#{term_to_find}%")
This will find results containing the string you searched. Example: you typed "bob" in the search, it could find "bobby" or even "Mr. Bob" (the ILIKE makes the search case-insensitive)
I'm trying to learn about SQL injections and have tried to implement these, but when I put this code in my controller:
params[:username] = "johndoe') OR admin = 't' --"
#user_query = User.find(:first, :conditions => "username = '#{params[:username]}'")
I get the following error:
Couldn't find all Users with 'id': (first, {:conditions=>"username = 'johndoe') OR admin = 't' --'"}) (found 0 results, but was looking for 2)
I have created a User Model with the username "johndoe", but I am still getting no proper response. BTW I am using Rails 4.
You're using an ancient Rails syntax. Don't use
find(:first, :condition => <condition>) ...
Instead use
User.where(<condtion>).first
find accepts a list of IDs to lookup records for. You're giving it an ID of :first and an ID of condition: ..., which aren't going to match any records.
User.where(attr1: value, attr2: value2)
or for single items
User.find_by(attr1: value, attr2: value)
Bear in mind that while doing all this, it would be valuable to check what the actual sql statement is by adding "to_sql" to the end of the query method (From what I remember, find_by just does a LIMIT by 1)
I am very new to Rails and am trying to create a search field to query my database. When I use the WHERE method and use "like", it works, but it's far too broad. If a person's last name is Smith, and I search for "i", it will return Smith as that string contains an "i". How can I make it more specific? I've searched around, read the docs and thought the following should work, but it doesn't return records.
this works
def self.search(query)
where("last_name like ? OR email like ?", "%#{query}%", "%#{query}%")
end
this does not
def self.search(query)
where("last_name = ? OR email = ?", "%#{query}%", "%#{query}%")
end
You're going to want to look more into SQL, as this is very specifically an SQL question. The % operator in SQL returns a fuzzy match. You want a strict match. So
def self.search(query)
where("last_name = ? OR email = ?", query, query)
end
or more succinctly:
scope :search, ->(query) { where('last_name = :query OR email = :query', query: query) }
I'm using postgres full text search with the pg_search gem. The search itself is working well, but I need to further filter the results and here are the details:
class Notebook < ActiveRecord::Base
has_many :invites
def self.text_search(query)
if query.present?
search(query)
else
scoped
end
end
Notebooks Controller:
def index
if params[:query].present?
#notebooks = Notebook.text_search(params[:query]).includes(:invites).where("invites.email = :email OR notebooks.access = :access OR notebooks.access = :caccess OR notebooks.user_id = :uid", email: current_user.email, access: "open", caccess: "closed", uid: current_user.id)
else
#notebooks = Notebook.includes(:invites).where("invites.email = :email OR notebooks.access = :access OR notebooks.access = :caccess OR notebooks.user_id = :uid", email: current_user.email, access: "open", caccess: "closed", uid: current_user.id)
end
The error I get is 'missing FROM-clause entry for table 'invites'. I have tried many different things including:
replacing 'includes' with 'joins'
replacing 'includes(:invites) with joins('LEFT JOIN "invites" ON "invites"."email" = "email" ')
changing the order of the .text_search and the .includes calls.
adding the includes call in the controller, in the model, in a scope, and in the text_search function definition.
I keep getting the same error, and when using the joins call with SQL it does not filter by invite emails, and shows multiple repeats of each search result.
I would just remove the include(:invites) because the text_search itself is working just fine. But I really need this condition to be included.
Any help would be greatly appreciated. Maybe I'm just getting my SQL call wrong, but I also would like to understand why the .includes(:invites) works without the pg text_search but won't work with it.
Edit #1 - more specific question
I think there are 2 slightly different questions here. The first seems to be some issue with combining pg_search gem and an 'includes(:invites)' call. The second question is what is the equivalent SQL statement that I can use in order to avoid making the 'includes(:invites)' call. I think it should be a LEFT JOIN of some sort, but I don't think I'm making it correctly. In my db, a Notebook has_many invites, and invites have an attribute 'email'. I need the the notebooks with invites that have an email equal to the current_user's email.
Help with either of these would be great.
Here is the link that showed me the solution to my problem:
https://github.com/Casecommons/pg_search/issues/109
Here is my specific code:
class Notebook < ActiveRecord::Base
has_many :invites
include PgSearch
pg_search_scope :search, against: [:title],
using: {tsearch: {dictionary: "english"}},
associated_against: {user: :name, notes:[:title, :content]}
scope :with_invites_and_access, lambda{ |c_user_email|
joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id').where('invites.email = ? OR notebooks.access = ? OR notebooks.access = ?', c_user_email, 'open', 'closed')
}
def self.text_search(query)
if query.present?
search(query).with_invites_and_access(current_user_email)
else
scoped
end
end
end
The key was the joins statement. joins(:invites) doesn't work, includes(:invites) doesn't work. The full SQL statement is required:
joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id')
I can see a join but I cannot see what makes joined invites fields to appear in the SELECT statement.
I think You may need to add the fields from the invites table into select() like this
select('invites.*').joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id').where('invites.email = ? OR notebooks.access = ? OR notebooks.access = ?', c_user_email, 'open', 'closed')
}
Im trying to build advanced search finder for my Candidate model.
Lets imagine it has couple fields + multiple associations like has_many: languages & has_many: skills. Now I'm building query like this:
query = Candidate.select("*")
if position_name
query = query.where('position_name LIKE ? OR position_name IS NULL',"%#{position_name}%")
end
if salary
query = query.where('salary <= ? OR salary IS NULL',salary)
end
and so on...
Now I want to add more advanced conditions like to find users who only have such skills like PHP and Java (so return only those users who have both skills)
This works but only when I insert OR
query = query.joins(:skills)
query = query.where('`skills`.`name` = ? OR `skills`.`name` = ?',"Java","PHP")
Additionally I'd like the same also for languages (plus, language have language.name & language.level)
Can someone points me in which direction to look? And also how to build such condition where I can multiple skills or multiple languages?
Have a look at the various search gems like Ransack, Metawhere or Searchlogic
http://rubygems.org/gems/ransack
https://github.com/railsdog/searchlogic
Both Ransack and Searchlogic allow searching on associated models and you can use scopes to restrict the search parameters.
Example Search params for Searchlogic.
[search][admitted_gte]
[search][admitted_lte]
[search][aetiology_like_any][] VIRUS
[search][at_risk_gte]
[search][at_risk_lte]
[search][died_gte]
[search][died_lte]
[search][gezi_reference_like]
[search][id]
[search][incidents_location_encrypted_postcode_like]
[search][lab_confirmed_gte]
[search][lab_confirmed_lte]
[search][onset_first_after]
[search][onset_first_before]
[search][onset_last_after]
[search][onset_last_before]
[search][outbreak_type_equals_any][] FOODBORNE
[search][point_source_date_after]
[search][point_source_date_before]
[search][total_affected_gte]
[search][total_affected_lte]
[search][user_reference_like]
[search][year_equals_any][] 2010
search[order] descend_by_id
Outbreak_Controller.rb Index action returns the results of the Search query. From 17 Search params only a single searchlogic call is required #search = Outbreak.search(params[:search]). The params are whitelisted against a list of allowed search params - code not shown.
def index
#set the default index order to be descending Outbreak id
if !params[:search][:order]
params[:search][:order] = "descend_by_id"
end
if params[:search][:bacterial_agents_bacterium_name_like_any] != nil && !params[:search][:bacterial_agents_bacterium_name_like_any].empty?
params[:search][:bacterial_agents_category_like] = "CAUSATIVE"
end
if params[:search][:viral_agents_virus_name_like_any] != nil && !params[:search][:viral_agents_virus_name_like_any].empty?
params[:search][:viral_agents_category_like] = "CAUSATIVE"
end
if params[:search][:protozoal_agents_protozoa_name_like_any] != nil && !params[:search][:protozoal_agents_protozoa_name_like_any].empty?
params[:search][:protozoal_agents_category_like] = "CAUSATIVE"
end
if params[:search][:toxic_agents_toxin_name_like_any] != nil && !params[:search][:toxic_agents_toxin_name_like_any].empty?
params[:search][:toxic_agents_category_like] = "CAUSATIVE"
end
#Outbreak.search takes all of the given params and runs it against the Outbreak model and it's associated models
#search = Outbreak.search(params[:search])
end