How to specify multiple values in where with AR query interface in rails3 - ruby-on-rails-3

Per section 2.2 of rails guide on Active Record query interface here:
which seems to indicate that I can pass a string specifying the condition(s), then an array of values that should be substituted at some point while the arel is being built. So I've got a statement that generates my conditions string, which can be a varying number of attributes chained together with either AND or OR between them, and I pass in an array as the second arg to the where method, and I get:
ActiveRecord::PreparedStatementInvalid: wrong number of bind variables (1 for 5)
which leads me to believe I'm doing this incorrectly. However, I'm not finding anything on how to do it correctly. To restate the problem another way, I need to pass in a string to the where method such as "table.attribute = ? AND table.attribute1 = ? OR table.attribute1 = ?" with an unknown number of these conditions anded or ored together, and then pass something, what I thought would be an array as the second argument that would be used to substitute the values in the first argument conditions string. Is this the correct approach, or, I'm just missing some other huge concept somewhere and I'm coming at this all wrong? I'd think that somehow, this has to be possible, short of just generating a raw sql string.

This is actually pretty simple:
Model.where(attribute: [value1,value2])

Sounds like you're doing something like this:
Model.where("attribute = ? OR attribute2 = ?", [value, value])
Whereas you need to do this:
# notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value)
Have a look at http://guides.rubyonrails.org/active_record_querying.html#array-conditions for more details on how this works.

Instead of passing the same parameter multiple times to where() like this
User.where(
"first_name like ? or last_name like ? or city like ?",
"%#{search}%", "%#{search}%", "%#{search}%"
)
you can easily provide a hash
User.where(
"first_name like :search or last_name like :search or city like :search",
{search: "%#{search}%"}
)
that makes your query much more readable for long argument lists.

Sounds like you're doing something like this:
Model.where("attribute = ? OR attribute2 = ?", [value, value])
Whereas you need to do this:
#notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value) Have a
look at
http://guides.rubyonrails.org/active_record_querying.html#array-conditions
for more details on how this works.
Was really close. You can turn an array into a list of arguments with *my_list.
Model.where("id = ? OR id = ?", *["1", "2"])
OR
params = ["1", "2"]
Model.where("id = ? OR id = ?", *params)
Should work

If you want to chain together an open-ended list of conditions (attribute names and values), I would suggest using an arel table.
It's a bit hard to give specifics since your question is so vague, so I'll just explain how to do this for a simple case of a Post model and a few attributes, say title, summary, and user_id (i.e. a user has_many posts).
First, get the arel table for the model:
table = Post.arel_table
Then, start building your predicate (which you will eventually use to create an SQL query):
relation = table[:title].eq("Foo")
relation = relation.or(table[:summary].eq("A post about foo"))
relation = relation.and(table[:user_id].eq(5))
Here, table[:title], table[:summary] and table[:user_id] are representations of columns in the posts table. When you call table[:title].eq("Foo"), you are creating a predicate, roughly equivalent to a find condition (get all rows whose title column equals "Foo"). These predicates can be chained together with and and or.
When your aggregate predicate is ready, you can get the result with:
Post.where(relation)
which will generate the SQL:
SELECT "posts".* FROM "posts"
WHERE (("posts"."title" = "Foo" OR "posts"."summary" = "A post about foo")
AND "posts"."user_id" = 5)
This will get you all posts that have either the title "Foo" or the summary "A post about foo", and which belong to a user with id 5.
Notice the way arel predicates can be endlessly chained together to create more and more complex queries. This means that if you have (say) a hash of attribute/value pairs, and some way of knowing whether to use AND or OR on each of them, you can loop through them one by one and build up your condition:
relation = table[:title].eq("Foo")
hash.each do |attr, value|
relation = relation.and(table[attr].eq(value))
# or relation = relation.or(table[attr].eq(value)) for an OR predicate
end
Post.where(relation)
Aside from the ease of chaining conditions, another advantage of arel tables is that they are independent of database, so you don't have to worry whether your MySQL query will work in PostgreSQL, etc.
Here's a Railscast with more on arel: http://railscasts.com/episodes/215-advanced-queries-in-rails-3?view=asciicast
Hope that helps.

You can use a hash rather than a string. Build up a hash with however many conditions and corresponding values you are going to have and put it into the first argument of the where method.

WRONG
This is what I used to do for some reason.
keys = params[:search].split(',').map!(&:downcase)
# keys are now ['brooklyn', 'queens']
query = 'lower(city) LIKE ?'
if keys.size > 1
# I need something like this depending on number of keys
# 'lower(city) LIKE ? OR lower(city) LIKE ? OR lower(city) LIKE ?'
query_array = []
keys.size.times { query_array << query }
#['lower(city) LIKE ?','lower(city) LIKE ?']
query = query_array.join(' OR ')
# which gives me 'lower(city) LIKE ? OR lower(city) LIKE ?'
end
# now I can query my model
# if keys size is one then keys are just 'brooklyn',
# in this case it is 'brooklyn', 'queens'
# #posts = Post.where('lower(city) LIKE ? OR lower(city) LIKE ?','brooklyn', 'queens' )
#posts = Post.where(query, *keys )
now however - yes - it's very simple. as nfriend21 mentioned
Model.where(attribute: [value1,value2])
does the same thing

Related

Better query using WHERE IN () when param can be nil

For example we have model TableRow - columns (:account_number, :month, :department, :phone_number). And have a method that returns filtered rows by arrays of this params.
For required params we can use
TableRow.where('account_number IN (?)', param)
Is there best way to add in this query unrequired params (department, phone_number) that can be nill and we should return records with any params in this column?
There are a couple ways to approach this. If you want your query to be static, you can check the literal value of your param with the SQL logic itself:
TableRow.where('COALESCE(:depts) IS NULL OR department IN (:depts)', depts: param)
You can also build up your relation incrementally in Ruby:
relation = TableRow.all
relation = relation.where(department: depts) if depts.present?
Your question is hard to understand, but if what you want is to filter by phone_number while still retrieving records where phone_number is null, you just have to that:
TableRow.where('phone_number IN (?)', param << nil)

using .where with .find

I am wondering how I can use where cause with the ActiveRecord find method.
Here is the code I am using:
Supplier.joins(:products).find(params[:id]).where('suppliers.permalink = ? AND variants.master = ?', params[:id], TRUE)
which gives me:
undefined method `where' for #<Supplier:0x007fe49b4eb330>
Supplier.joins(:products).find(params[:id]).where('suppliers.permalink = ? AND variants.master = ?', params[:id], TRUE)
What you're doing here is finding the first record with the id contained in params[:id], then trying to run a where statement on that single record. where only works when run against the model itself.
The confusing part here is that you are using params[:id] both for the primary key (find searches the id field) but then also comparing it to the permalink column in the where clause.
To explain the usage of both methods:
find will search for result(s) from the table, matching the argument you provide it to the id field. You can pass in multiple id's and this method is mostly used to select a row that you know exists, by id. Most commonly it is used with a single id and returns a single instance.
where is used to find all results from the table that match the clause and return a collection of records. You can then refine these results or select one, for example by using .first:
Supplier.joins(:products).where('suppliers.permalink = ? AND variants.master = ?', params[:permalink], true).first
(Note that you're using joins(:products) but then querying variants table. Is this incorrect?)
Supplier.joins(:products).where('suppliers.permalink = ? AND variants.master = ?', params[:id], TRUE).find(params[:id])

doctrine native sql not accepting parameter list

I'm trying to do native SQL in Doctrine. Basically I have 2 parameters:
CANDIDATE_ID - user for who we delete entries,
list of FILE_ID to keep
So I make
$this->getEntityManager()->getConnection()->
executeUpdate( "DELETE FROM FILE WHERE CANDIDATE_ID = :ID AND NOT ID IN :KEEPID",
array(
"ID" => $candidate->id,
"KEEPID" => array(2) )
);
But Doctrine fails:
Notice: Array to string conversion in D:\xampp\htdocs\azk\vendor\doctrine\dbal\lib\Doctrine\DBAL\Connection.php on line 786
Is this bug in Doctrine? I'm making somewhere else select with IN but with QueryBuilder and it's working. Maybe someone could suggest better way of deleting entries, with QueryBuilder for example?
$stmt = $conn->executeQuery('SELECT * FROM articles WHERE id IN (?)',
array(array(1, 2, 3, 4, 5, 6)),
array(\Doctrine\DBAL\Connection::PARAM_INT_ARRAY)
);
From Doctrine's documentation.
You can't pass an array of IDs to a parameter. You can do this for scalar values, but even if this had a 'toString', it wouldn't be what you want.
String concatenation is one method,
"DELETE FROM FILE WHERE CANDIDATE_ID = :ID AND NOT ID IN (". implode(",", $list_of_ids) .")"
But this method goes straight around parameters, and therefore suffers in terms of readability, and is limited to a certain maximum line length, which can vary between databases.
Another approach is to write a function returning a table result, which takes a string of IDs as a parameter.
You could also solve this with a join to a table containing the IDs to keep.
It's a problem I've seen many times with few good answers, but it's usually caused by a misunderstanding in the way the database is modelled. This is a 'code smell' for database access.

Best way to combine first_name and last_name columns in Ruby on Rails 3?

I have a method in my User model:
def self.search(search)
where('last_name LIKE ?', "%#{search}%")
end
However, it would be nice for my users to be able to search for both first_name and last_name within the same query.
I was thinking to create a virtual attribute like this:
def full_name
[first_name, last_name].join(' ')
end
But is this efficient on a database level. Or is there a faster way to retrieve search results?
Thanks for any help.
Virtual attribute from your example is just class method and cannot be used by find-like ActiveRecord methods to query database.
Easiest way to retrive search result is modifying Search method:
def self.search(search)
q = "%#{query}%"
where("first_name + ' ' + last_name LIKE ? OR last_name + ' ' + first_name LIKE ?", [q, q])
end
where varchar concatenation syntax is compatible with your database of choice (MS SQL in my example).
The search functionality, in your example, is still going to run at the SQL level.
So, to follow your example, your search code might be:
def self.search_full_name(query)
q = "%#{query}%"
where('last_name LIKE ? OR first_name LIKE ?', [q, q])
end
NOTE -- these sorts of LIKE queries, because they have a wildcard at the prefix, will be slow on large sets of data, even if they are indexed.
One way this can be implemented is by tokenizing (splitting) the search query and creating one where condition per each token:
def self.search(query)
conds = []
params = {}
query.split.each_with_index do |token, index|
conds.push "first_name LIKE :t#{index} OR last_name LIKE :t#{index}"
params[:"t#{index}"] = "%#{token}%"
end
where(conds.join(" OR "), params)
end
Also make sure you prevent SQL injection attacks.
However, it's better to use full-text searching tools, such as ElasticSearch and its Ruby gem named Tire to handle searches.
EDIT: Fixed the code.
A scope can be made to handle complex modes, here's an example from one project I'm working on:
scope :search_by_name, lambda { |q|
if q
case q
when /^(.+),\s*(.*?)$/
where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{$1}%","%#{$1}%","%#{$2}%","%#{$2}%","%#{$2}%"
])
when /^(.+)\s+(.*?)$/
where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{$2}%","%#{$2}%","%#{$1}%","%#{$1}%","%#{$1}%"
])
else
where(["(last_name LIKE ? or maiden_name LIKE ? OR first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{q}%","%#{q}%","%#{q}%","%#{q}%","%#{q}%"
])
end
else
{}
end
}
As you can see, I do a regex match to detect different patterns an build different searches depending on what is provided. As an added bonus, if nothing is provided, it returns an empty hash which effectively is where(true) and returns all results.
As mentioned elsewhere, the db cannot index the columns when a wildcard is used on both sides like %foo%, so this could potentially get slow on very large datasets.

what does positional and named parameter in a query mean?

here we got a positional parameter:
SELECT
u
FROM ForumUser u
WHERE u.id = ?1
and here a named parameter:
SELECT
u
FROM ForumUser u
WHERE u.username = :name
this is DQL (doctrine query language) but i think the concept is the same.
could someone please explain what these mean and do?
A positional parameter is set by its index in the clause.
A named parameter is set by its name.
When you are setting the values, you might have the values in an array, in which case the positional form could me more useful. Alternatively, you might have them in an associative array by name, in which case the named form is more useful.
Update - Although the documentation refers to positional parameters as for example ?1, the examples just use ?.
This example of positional parameters maps values by position in the array provided into the positional placeholders in the query.
$q = Doctrine_Query::create()
->from('User u')
->where('u.username = ? and u.age = ?', array('Arnold', 50));
$users = $q->fetchArray();
However this example maps values by name in an associative array to their named placeholders. See how they do not need tgo be in order.
$q = Doctrine_Query::create()
->from('User u')
->where('u.username = :username and u.age = :age',
array(':age' => 50, ':username' => 'Arnold'));
(Have to admit I'm not a PHP guy - above based on the examples here.)
Positional parameters are specified by their order in the query. Named parameters are specified by their names.
When using positional parameters you have to add them in the same order that they are used in the query, and if you want to use the same value more than once you have to add it multiple times as separate parameters.
When using names parameters you can add them in any order you want, and a parameter can be used more than once in the query.
For example if you have a query that searches in several fields, using positional parameters it could look like this:
select u.UserId, u.UserName
from FormumUser u
where u.UserName like ? or u.Email like ? or u.Address like ?
You would have to add the search string three times as separate parameters. Using names parameters it could look like:
select u.UserId, u.UserName
from FormumUser u
where u.UserName like #find or u.Email like #find or u.Address like #find
Then you would only add one parameter, as the query can use the same parameter in three places.
(The exact syntax for using the parameters in the query of course varies depending on what database solution you are using.)
I don't kown if I Understood right, so this is what I think:
Positional parameters should be indexed using an integer index, and named parameters should be accessed though their names.
Example (this is pseudocode):
query.SetParameter(0, 456); // here we set value 456 to the first parameter, which has index zero
query.SetParameter("username", "John Smith"); // here we set value "John Smith" to the parameter named "username"