Ruby on Rails query where equal to two concatenated columns - ruby-on-rails

My customer model has a first_name and last_name field. I can query the model with the following code:
Customer.where(first_name: "John").where(last_name "Doe")
However, I want to query the model like this:
Customer.where(full_name: "John Doe")
But I do not have a full_name column. How can I accomplish this without creating a full_name field?

You can use the CONCAT operator in SQL:
Customer.where("CONCAT(first_name, ' ', last_name) = ?", full_name)
Which actually might make sense if you are creating something like a search function with like/ilike:
Customer.where("CONCAT(first_name, ' ', last_name) LIKE ?", "%#{full_name}%")

You can not, if you do not have a full name attribute you can not query by a full name and any try to do it using ActiveRecord in a single query will add an extra complexity to your app (or model), but to solve your question with simplicity you can add a scope to your Customer model, or a class method passing the first and last name as argument.
Using a scope
scope :by_full_name, (lambda { |first_name, last_name|
where(first_name: first_name, last_name: last_name)
})
using a static (class) method:
def self.by_full_name(first_name, last_name)
where(first_name: first_name, last_name: last_name)
end
Personally, due to simplicity and clearly code I prefer to use scopes instead of class methods if no extra logic is required.

As an alternative to Max, split the name and query on both fields as in your question.
scope :where_full_name, ->(full_name) do
(first_name, last_name) = full_name.split(/\s+/)
where first_name: first_name, last_name: last_name
end
Then use
Customer.where_full_name 'Betty Davis'
This won't work for names like Joan van der Graff.

Related

How to query ActiveRecord for 2 attributes with 1 value

I have a username in this variable invoice.user_name, say John Wayne Doe. I want to find a User based on his/her name. Ideally, I would like to use User.where(name: invoice.user_name). However, my database has 'first_name' and 'last_name', so I can't query based on name. Would it be possible to do something like this: User.where('first_name AND last_name like ?', invoice.user_name')
Or how would I approach that?
Thx
You can do it this way... or at least it works for me using a postgreSQL database...
User.where("CONCAT(first_name, ' ', last_name) = ?", invoice.user_name)

RoR Filter Out Current User with Join, Select and Order

I have the following statement in an RoR controller called nominations:
#users = User.joins(:department).select("CONCAT(last_name, ', ', first_name,
' - ', name) as user_dept, last_name, first_name, name,
users.id").order("last_name, first_name, middle_name")
In the view, I have this to put this in a drop down:
<%= select_tag "nomination[user_id]", options_from_collection_for_select(#users,
:id, :user_dept), prompt: "Select User" %>
I want to filter out the current user so that someone can't nominate themselves. I've seen several articles about using where.not or scope (for example) but all of the statements I could find are basic selections from one table. How do I define and use a scope while preserving all of the other stuff? I need that join and formatting.
Edit - Whenever I try where.not anywhere in the controller or model, I get the error "wrong number of arguments (0 for 1)".
This should do the trick:
#users = ...
#users = #users.where.not(users: {id: current_user.id})
Note that you need to specify name of the table (not the model) when you do the join, otherwise database will have no idea which id column it should look for (users.id vs departments.id).
Before Rails 4
not method is quite a new thing and is not available in Rails 3. (In fact, where method wasn't expecting to be called without arguments, that's why the error you got is unexpected number of arguments (0 for 1)).
Those were dark times when we had to use the following monstrosity to get stuff working:
#users = ...
#users = #users.where('users.id != ?', current_user.id)
You need a scope in the User model:
class User < ActiveRecord::Base
scope :except_id, -> (id) ( where.not(id: id) )
end
Then you can freely use it everywhere you want:
#users = User.except_id(current_user.id).joins(:department).select("...")
#users = User.where.not(id: current_user.id).joins(:department)
.select("CONCAT(last_name, ', ', first_name, ' - ', name) as user_dept, last_name, first_name, name, users.id")
.order("last_name, first_name, middle_name")

String concatenation of two attributes in Rails where query

Let's say I have the attrs first_name and last_name for a User. I know the user's full name is "Johnny Appleseed", as I have a large Hash of user's full names as strings.
Sure, I could split each string first, and search like so:
User.where(first_name: 'Johnny', last_name: "Appleseed")
BUT, I'm wondering if there is a way to basically concat the two in a query, essentially like so:
User.where('first_name + last_name = ?', 'Johnny Appleseed')
Or if I could have a full_name method on the User model, and search by that?
def full_name
"#{self.first_name} #{self.last_name}"
end
User.where('full_name = ?', 'Johnny Appleseed')
Probably the easiest solution would be:
User.where(%q(concat_ws(' ', first_name, last_name) = ?), 'Johnny Appleseed')

Rails ActiveRecord - Search on Multiple Attributes

I'm implementing a simple search function that should check for a string in either the username, last_name and first_name. I've seen this ActiveRecord method on an old RailsCast:
http://railscasts.com/episodes/37-simple-search-form
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
But how do I make it so that it searches for the keyword in name, last_name and first name and returns the record if the one of the fields matched the term?
I'm also wondering if the code on the RailsCast is prone to SQL injections?
Thanks a lot!
I assumed your model name is Model - just replace it with your real model name when you do the actual query:
Model.where("name LIKE ? OR last_name LIKE ? OR first_name LIKE ?", "%#{search}%","%#{search}%","%#{search}%")
About your worries about SQL injections - both of code snippets are immune to SQL injections. As long as you do not directly embed strings into your WHERE clause you are fine. An example for injection-prone code would be:
Model.where("name LIKE '#{params[:name]}'")
Although the selected answer will work, I noticed that it breaks if you try to type a search "Raul Riera" because it will fail on both cases, because Raul Riera is not either my first name or my last name.. is my first and last name... I solved it by doing
Model.where("lower(first_name || ' ' || last_name) LIKE ?", "%#{search.downcase}%")
With Arel, you can avoid writing the SQL manually with something like this:
Model.where(
%i(name first_name last_name)
.map { |field| Model.arel_table[field].matches("%#{query}%")}
.inject(:or)
)
This would be particularly useful if the list of fields to match against was dynamic.
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
The best way to do this is:
Model.where("attr_a ILIKE :query OR attr_b ILIKE :query", query: "%#{query}%")

Rails gem rails3-jquery-autocomplete: How do I query multiple fields

I'm using the rails3-jquery-autocomplete gem found here: http://github.com/crowdint/rails3-jquery-autocomplete
The instructions are clear for how to query a single attribute of a model and I am able to make that work without a problem.
My Person model has two attributes that I would like to combine and query, however. They are first_name and last_name. I would like to combine them into a pseudo-attribute called full_name. Currently, I receive this error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: full_name: SELECT "people".* FROM "people" WHERE (LOWER(full_name) LIKE 'cla%') ORDER BY full_name ASC LIMIT 10):
There is no full_name attribute of the Person model, though I have the following method in the Person model file:
def full_name
"#{self.first_name} #{self.last_name}"
end
How do I modify the Person model file so that calls to full_name queries the database to match a combination of first_name and last_name?
Your pseudo attribute works only on records already retrieved, but it has no bearing on searching for records. Probably the easiest solution is a named named scope like:
scope :search_by_name, lambda { |q|
(q ? where(["first_name LIKE ? or last_name LIKE ? or concat(first_name, ' ', last_name) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
Thus, a call like:
Person.search_by_name(params[:q])
will return an appropriate result set. It will also return all entries if no param was passed (or more specifically, this scope will add nothing extra), making it an easy drop-in for the index action.
Sadly, the scope method mentioned above didn't work for me. My solution was to simply overwrite the get_autocomplete_items method (formerly get_items).
For what it's worth, there is a MySQL function (other db's have it as well, but we're talking MySQL for the moment) that is better suited to the type of concatenation you're using:
def get_autocomplete_items(parameters)
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
end
MySQL's CONCAT_WS() is intended to join strings together with some sort of separator and is ideal for full names.
This chunk of code basically says return the "first last" formatted names of contacts that match whatever the user is searching by when we pair up the database's contact records by a concatenated pairs of first and last names. I feel it's better than the SQL statement above since it does a full search that will match first AND/OR last name in one statement, not three OR statements.
Using this "hn sm" would match "John Smith" since indeed "hm sm" is LIKE "John Smith". Furthermore, it has the added benefit of also returning the concatenated first and last name of each contact. You may want the full record. If that's the case, remove the select() query from the line above. I personally had the need for the user to search for a name and have an autocomplete field return all possible matches, not the records.
I know this is a bit late, but I hope it helps someone else!
Full implementation of multiple-field autocomplete :
Following my comment, my solution to integrate into jquery-autocomplete was to have a custom implementation of the "internal" autocomplete.
1. Query the database
If you're using ActiveRecord, you can use DGM's solution for your named_scope
If you're using Mongoid, you can use this syntax instead:
scope :by_first_name, ->(regex){
where(:first_name => /#{Regexp.escape(regex)}/i)
}
scope :by_last_name, ->(regex){
where(:last_name => /#{Regexp.escape(regex)}/i)
}
scope :by_name, ->(regex){
any_of([by_first_name(regex).selector, by_last_name(regex).selector])
}
EDIT : if you want a stronger autocomplete that can handle accents, matching parts of text, etc; you should try the mongoid text index. Credits to the original answer there
index(first_name: 'text', last_name: 'text')
scope :by_name, ->(str) {
where(:$text => { :$search => str })
}
And don't forget to build the indexes after adding that rake db:mongoid:create_indexes
So you can basically do User.by_name(something)
2. Create an autocomplete action in your controller
Because the one provided by jquery-autocomplete... ain't gonna do what we want.
Note that you'll have to convert the result to JSON so it can be used in the frontend jquery-autocomplete. For this I have chosen to use the gem ActiveModel::Serializer, but feel free to use something else if you prefer, and skip step 3
In your controller :
def autocomplete
#users = User.by_name(params[:term])
render json: #users, root: false, each_serializer: AutocompleteSerializer
end
3. Reformat the response
Your serializer using the gem activemodel:
I provided the link to the 0.9 version, as the master mainpage doesn't contain the full documentation.
class AutocompleteSerializer < ActiveModel::Serializer
attributes :id, :label, :value
def label
object.name
# Bonus : if you want a different name instead, you can define an 'autocomplete_name' method here or in your user model and use this implementation
# object.respond_to?('autocomplete_name') ? object.autocomplete_name : object.name
end
def value
object.name
end
4. Create a route for your autocompletion
In your routes :
get '/users/autocomplete', to: 'users#autocomplete', as: 'autocomplete_user'
5. Have fun in your views
Finally in your views you can use the default syntax of jquery-rails, but remember to change the path !
<%= form_tag '' do
autocomplete_field_tag 'Name', '', autocomplete_user_path, :id_element => "#{your_id}", class: "form-control"
end %>
RQ : I used some 2-level deep nested forms, so it was a bit tricky to get the right id element your_id. In my case I had to do somethig complicated, but most likely it will be simple for you. You can always have a look at the generated DOM to retrieve the field ID
This is a hack and I would very much like this function to be included in the gem but as slant said overwriting the get_autocomplete_items works as he wrote it but it will only return first_name and last_name from the model column. In order to restore functionality that frank blizzard asked for you also need to return the id of the row.
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name, id").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
The difference between mine and slant's answer is id as the last argument of the select method. I know it is late but I hope it helps somebody in the future.
If you don't need the functionality of comparing your search string against the concatenated string and are trying to just do a query on three separate columns you can use this fork of the gem : git://github.com/slash4/rails3-jquery-autocomplete.git. Oh also that fork will only work with ActiveRecord which is probably why they didn't pull it.
To perform a case-insensitive search using #Cyril's method for rails4-autocomplete, I did a slight modification to the named scope #DGM provided
scope :search_by_name, lambda { |q|
q.downcase!
(q ? where(["lower(first_name) LIKE ? or lower(last_name) LIKE ? or concat(lower(first_name), ' ', lower(last_name)) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
This converts the record's entry to lowercase and also converts the search string to lowercase as it does the comparison
An alternative to the previous suggestions is to define a SQL view on the table holding first_name and last_name. Your SQL code (in SQLite) might look like:
CREATE VIEW Contacts AS
SELECT user.id, ( user.first_name || ' ' || users.last_name ) AS fullname
FROM persons;
Then define a model for the table:
class Contact < ActiveRecord::Base
set_table_name "contacts"
end
You can now use rails3-jquery-autocomplete 'out-of-the box'. So in your controller you would write:
autocomplete :contact, :fullname
In your view file you can simply write:
f.autocomplete_field :contact, autocomplete_contact_fullname_path
I will leave configuration of the route as an exercise for the reader :-).

Resources