How can .where accept an array without braces? - ruby-on-rails

I'm not sure how is this implemented, when you do something like:
Model.where(["subjects = ?", 1])
Rails allows you to omit the braces:
Model.where("subjects = ?", 1)
I know this is possible with hashes, but how is it possible so you can pass ANY number of arguments (you can have 100 question marks if you want) and for Rails to still interpret this as an array?

In Ruby a method can accept splat arguments.
def foo(*a)
a
end
foo('bar', 'baz')
# => ["bar", "baz"]
The splat gathers up any remaining arguments. You can even use it with regular arguments:
def foo(a, *b)
b
end
foo('bar', 'baz')
# => ["baz"]
You can even do something like:
def foo(*a)
a.length == 1 && a.first.is_a?(Array) ? a.first : a
end
Now calling foo('bar', 'baz') and foo(['bar', 'baz']) have the same return value.
However if what you want is a WHERE condition where the value can be one of many possible values you would write it like so:
Model.where(foo: [1, 2, 3, 5])
Which would create a WHERE models.foo IN (1,2,3,5) clause.

From the Docs
Model.where(array)
If an array is passed, then the first element of the array is treated as a template, and the remaining elements are inserted into the template to generate the condition. Active Record takes care of building the query to avoid injection attacks, and will convert from the ruby type to the database type where needed. Elements are inserted into the string in the order in which they appear.
User.where(["name = ? and email = ?", "Joe", "joe#example.com"])
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe#example.com';

Related

Sending array of values to a sql query in ruby?

I'm struggling on what seems to be a ruby semantics issue. I'm writing a method that takes a variable number of params from a form and creates a Postgresql query.
def self.search(params)
counter = 0
query = ""
params.each do |key,value|
if key =~ /^field[0-9]+$/
query << "name LIKE ? OR "
counter += 1
end
end
query = query[0..-4] #remove extra OR and spacing from last
params_list = []
(1..counter).each do |i|
field = ""
field << '"%#{params[:field'
field << i.to_s
field << ']}%", '
params_list << field
end
last_item = params_list[-1]
last_item = last_item[0..-3] #remove trailing comma and spacing
params_list[-1] = last_item
if params
joins(:ingredients).where(query, params_list)
else
all
end
end
Even though params_list is an array of values that match in number to the "name LIKE ?" parts in query, I'm getting an error: wrong number of bind variables (1 for 2) in: name LIKE ? OR name LIKE ? I tried with params_list as a string and that didn't work any better either.
I'm pretty new to ruby.
I had this working for 2 params with the following code, but want to allow the user to submit up to 5 ( :field1, :field2, :field3 ...)
def self.search(params)
if params
joins(:ingredients).where(['name LIKE ? OR name LIKE ?',
"%#{params[:field1]}%", "%#{params[:field2]}%"]).group(:id)
else
all
end
end
Could someone shed some light on how I should really be programming this?
PostgreSQL supports standard SQL arrays and the standard any op (...) syntax:
9.23.3. ANY/SOME (array)
expression operator ANY (array expression)
expression operator SOME (array expression)
The right-hand side is a parenthesized expression, which must yield an array value. The left-hand expression is evaluated and compared to each element of the array using the given operator, which must yield a Boolean result. The result of ANY is "true" if any true result is obtained. The result is "false" if no true result is found (including the case where the array has zero elements).
That means that you can build SQL like this:
where name ilike any (array['%Richard%', '%Feynman%'])
That's nice and succinct so how do we get Rails to build this? That's actually pretty easy:
Model.where('name ilike any (array[?])', names.map { |s| "%#{s}%" })
No manual quoting needed, ActiveRecord will convert the array to a properly quoted/escaped list when it fills the ? placeholder in.
Now you just have to build the names array. Something simple like this should do:
fields = params.keys.select { |k| k.to_s =~ /\Afield\d+\z/ }
names = params.values_at(*fields).select(&:present)
You could also convert single 'a b' inputs into 'a', 'b' by tossing a split and flatten into the mix:
names = params.values_at(*fields)
.select(&:present)
.map(&:split)
.flatten
You can achieve this easily:
def self.search(string)
terms = string.split(' ') # split the string on each space
conditions = terms.map{ |term| "name ILIKE #{sanitize("'%#{term}%'")}" }.join(' OR ')
return self.where(conditions)
end
This should be flexible: whatever the number of terms in your string, it should returns object matching at least 1 of the terms.
Explanation:
The condition is using "ILIKE", not "LIKE":
"ILIKE" is case-insensitive
"LIKE" is case-sensitive.
The purpose of the sanitize("'%#{term}%'") part is the following:
sanitize() will prevent from SQL injections, such as putting '; DROP TABLE users;' as the input to search.
Usage:
User.search('Michael Mich Mickey')
# can return
<User: Michael>
<User: Juan-Michael>
<User: Jean michel>
<User: MickeyMouse>

find_all elements in an array that match a condition?

I've an array of hash entries, and want to filter based on a paramater passed into the function.
If there are three values in the hash, A, B, and C, I want to do something similar to:
data = [{A:'a1', B:'b1', C:'c1'},
{A:'a1', B:'b2', C:'c1'},
{A:'a1', B:'b2', C:'c2'},
{A:'a2', B:'b1', C:'c1'},
{A:'a2', B:'b2', C:'c1'}]
data.find_all{ |d| d[:A].include?params[:A] }
.find_all{ |d| d[:B].include?params[:B] }
.find_all{ |d| d[:C].include?params[:C] }
find all where A == 'a1' AND B='b2'
so for above I get:
{A:'a1', B:'b2', C:'c1'} and {A:'a1', B:'b2', C:'c2'}
* put if / else statements, like params.has_key? 'B' then do something.
* modify my code each time a new type of value is added to the Hash map (say now I have 'D').
Note: The key of the hash is a symbol, but the value is a string and I want to do a "contains" not "equals".
I think of it as SQL Select statement with where zero or more '==' clause
If I understand your question correctly, then I believe that the select method of Array class may be helpful to you.
select takes a block of code which is intended to be a test condition on each element in your array. Any elements within your array which satisfy that test condition will be selected, and the result is an array of those selected elements.
For example:
arr = [ 4, 8, 15, 16, 23, 42 ]
result = arr.select { |element| element > 20 }
puts result # prints out [23, 42]
In your example, you have an array of hashes, which makes it only slightly more complicated than my example with a simple array of numbers. In your example, we have:
data = [{A:'a1', B:'b1', C:'c1'},
{A:'a1', B:'b2', C:'c1'},
{A:'a1', B:'b2', C:'c2'},
{A:'a2', B:'b1', C:'c1'},
{A:'a2', B:'b2', C:'c1'}]
I believe what you want your code to do is something like: Select from my data array all of the hashes where, within the hash, :A equals some value AND :B equals some other value.
Let's say you want to find all of the hashes where :A == 'a1' and :B == 'b2'. You would do that like this:
data.select { |hash_element| hash_element[:A] == 'a1' && hash_element[:B] == 'b2' }
This line returns to you an array with those hashes from your original data array which satisfy the condition we provided in the block - that is, those hashes where :A == 'a1' and :B == 'b2'. Hope that that helps shed some light on your problem!
More information about the select method here:
http://www.ruby-doc.org/core-2.1.0/Array.html#method-i-select
edited - below is an addition to original answer
To follow up on your later question about if/else clauses and the addition of new parameters... the block of code that you pass to select can, of course, be much more complicated than what I've written in the example. You just need to keep this in mind: If the last line of the block of code evaluates to true, then the element will be selected into the result array. Otherwise, it won't be selected.
So, that means you could define a function of your own, and call that function within the condition block passed to select. For example, something like this:
def condition_test(hash_element, key_values)
result = true
key_values.each do |pair|
if hash_element[pair[:key]] != pair[:value]
result = false
end
end
return result
end
# An example of the key-value pairs you might require to satisfy your select condition.
requirements = [ {:key => :A, :value => 'a1'},
{:key => :B, :value => 'b2'} ]
data.select { |hash_element| condition_test(hash_element, requirements) }
This makes your condition block a bit more dynamic, rather than hard-wired for :A == 'a1' and :B == 'b2' like we did in the earlier example. You can tweak requirements on the fly based on the keys and values that you need to look for. You could also modify condition_test to do more than just check to see if the hash value at some key equals some value. You can add in your if/else clauses in condition_test to test for things like the presence of some key, etc.
I think you want to use .values_at(key)
http://www.ruby-doc.org/core-2.1.0/Hash.html#method-i-values_atvalues_at method

What's the difference between "=" & "=>" and "#variable", "##variable" and ":variable" in ruby?

I know these are the basics of rails but i still don't know the full difference between = sign and => and the difference between #some_variable, ##some_variable and :some_variable in rails.
Thanks.
OK.
The difference between the = and the => operators is that the first is assignment, the second represents an association in a hash (associative array). So { :key => 'val' } is saying "create an associative array, with :key being the key, and 'val' being the value". If you want to sound like a Rubyist, we call this the "hashrocket". (Believe it or not, this isn't the most strange operator in Ruby; we also have the <=>, or "spaceship operator".)
You may be confused because there is a bit of a shortcut you can use in methods, if the last parameter is a hash, you can omit the squiggly brackets ({}). so calling render :partial => 'foo' is basically calling the render method, passing in a hash with a single key/value pair. Because of this, you often see a hash as the last parameter to sort of have a poor man's optional parameters (you see something similar done in JavaScript too).
In Ruby, any normal word is a local variable. So foo inside a method is a variable scoped to the method level. Prefixing a variable with # means scope the variable to the instance. So #foo in a method is an instance level.
## means a class variable, meaning that ## variables are in scope of the class, and all instances.
: means symbol. A symbol in Ruby is a special kind of string that implies that it will be used as a key. If you are coming from C#/Java, they are similar in use to the key part of an enum. There are some other differences too, but basically any time you are going to treat a string as any sort of key, you use a symbol instead.
Wow, a that's a lot of different concepts together.
1) = is plain old assignment.
a = 4;
puts a
2) => is used to declare hashes
hash = {'a' => 1, 'b' => 2, 'c' => 3}
puts hash['b'] # prints 2
3) #var lets you access object instance variable.
class MyObject
def set_x(x)
#x = x
end
def get_x
#x
end
end
o = MyObject.new
o.set_x 3
puts o.get_x # prints 3
4) ##var lets you access class ('static') variables.
class MyObject
def set_x(x)
##x = x # now you can access '##x' from other MyObject instance
end
def get_x
##x
end
end
o1 = MyObject.new
o1.set_x 3
o2 = MyObject.new
puts o2.get_x # prints 3, even though 'set_x' was invoked on different object
5) I usually think of :var as special 'label' class. Example 2 can be rephrased like this
hash = {:a => 1, :b => 2, :c => 3}
puts hash[:b] # prints 2

Is it possible to have variable find conditions for both the key and value?

I'm trying to pass in both the field and the value in a find call:
#employee = Employee.find(:all,
:conditions => [ '? = ?', params[:key], params[:value].to_i)
The output is
SELECT * FROM `employees` WHERE ('is_manager' = 1)
Which returns no results, however when I try this directly in mysqsl using the same call without the '' around is_manager, it works fine. How do I convert my params[:key] value to a symbol so that the resulting SQL call looks like:
SELECT * FROM `employees` WHERE (is_manager = 1)
Thanks,
D
If you want to convert a string to symbol(which is what params[:key] produces, all you need to do is
params[:key].to_s.to_sym
2 points:
A word of caution : symbols are
not garbage collected.
Make sure your key is not a
number, if you convert to_s first
then to_sym, your code will work but
you may get a wierd symbol like
this:
:"5"
You could use variable substitution for column name instead of using bind values:
# make sure the key passed is a valid column
if Employee.columns_hash[params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
You can further secure the solution by ensuring column name passed belongs to a pre selected set.:
if ["first_name", "last_name"].include? [params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
"string".to_sym

Clean way to find ActiveRecord objects by id in the order specified

I want to obtain an array of ActiveRecord objects given an array of ids.
I assumed that
Object.find([5,2,3])
Would return an array with object 5, object 2, then object 3 in that order, but instead I get an array ordered as object 2, object 3 and then object 5.
The ActiveRecord Base find method API mentions that you shouldn't expect it in the order provided (other documentation doesn't give this warning).
One potential solution was given in Find by array of ids in the same order?, but the order option doesn't seem to be valid for SQLite.
I can write some ruby code to sort the objects myself (either somewhat simple and poorly scaling or better scaling and more complex), but is there A Better Way?
It's not that MySQL and other DBs sort things on their own, it's that they don't sort them. When you call Model.find([5, 2, 3]), the SQL generated is something like:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
This doesn't specify an order, just the set of records you want returned. It turns out that generally MySQL will return the database rows in 'id' order, but there's no guarantee of this.
The only way to get the database to return records in a guaranteed order is to add an order clause. If your records will always be returned in a particular order, then you can add a sort column to the db and do Model.find([5, 2, 3], :order => 'sort_column'). If this isn't the case, you'll have to do the sorting in code:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Based on my previous comment to Jeroen van Dijk you can do this more efficiently and in two lines using each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
For reference here is the benchmark i used
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
end
end.real
#=> 4.45757484436035 seconds
Now the other one
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
ids.collect {|id| results.detect {|result| result.id == id}}
end
end.real
# => 6.10875988006592
Update
You can do this in most using order and case statements, here is a class method you could use.
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
# User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id)
# #=> [3,2,1]
Apparently mySQL and other DB management system sort things on their own. I think that you can bypass that doing :
ids = [5,2,3]
#things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
A portable solution would be to use an SQL CASE statement in your ORDER BY. You can use pretty much any expression in an ORDER BY and a CASE can be used as an inlined lookup table. For example, the SQL you're after would look like this:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
That's pretty easy to generate with a bit of Ruby:
ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
The above assumes that you're working with numbers or some other safe values in ids; if that's not the case then you'd want to use connection.quote or one of the ActiveRecord SQL sanitizer methods to properly quote your ids.
Then use the order string as your ordering condition:
Object.find(ids, :order => order)
or in the modern world:
Object.where(:id => ids).order(order)
This is a bit verbose but it should work the same with any SQL database and it isn't that difficult to hide the ugliness.
As I answered here, I just released a gem (order_as_specified) that allows you to do native SQL ordering like this:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Just tested and it works in SQLite.
Justin Weiss wrote a blog article about this problem just two days ago.
It seems to be a good approach to tell the database about the preferred order and load all records sorted in that order directly from the database. Example from his blog article:
# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << "WHEN #{id} THEN #{index} "
end
order_clause << "ELSE #{ids.length} END"
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)
That allows you to write:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
Here's a performant (hash-lookup, not O(n) array search as in detect!) one-liner, as a method:
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
Another (probably more efficient) way to do it in Ruby:
ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record|
result[record.id] = record
result
end
sorted_records = ids.map {|id| records_by_id[id] }
Here's the simplest thing I could come up with:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
#things = [5,2,3].map{|id| Object.find(id)}
This is probably the easiest way, assuming you don't have too many objects to find, since it requires a trip to the database for each id.

Resources