how to match all multiple conditions with rails where method - ruby-on-rails

Using active record, I want to perform a lookup that returns a collection of items that have ALL matching id's.
Given that the below example matches on ANY id in the array, I am trying to figure out the syntax so that it will match when ALL of the id's match. (given that in this example there is a many to many relationship).
The array length of the id's is also variable which prohibits chaining .where()
x.where(id: [1,2])
Note: this question got removed before and there are a lot of answers for performing a sql "where in" but this question is about performing a sql "where and"

You can use exec_query and execute your own bound query:
values = [1, 2]
where_condition = values.map.with_index(1) { |_, index| "id = $#{index}" }.join(" AND ")
sql = "SELECT * FROM table WHERE #{ where_condition }"
binds = values.map { |i| ActiveRecord::Relation::QueryAttribute.new(nil, i, ActiveRecord::Type::Integer.new) }
ActiveRecord::Base.connection.exec_query(sql, nil, binds)

I completely agree with #muistooshort's comment
where(id: [1,2]) doesn't make sense unless you're joining to an association table and in that case,..."where in" combined with HAVING [solves your problem].
But for the sake of answering the question and the assumption that id was just and example.
While #SebastianPalma's answer will work it will return an ActiveRecord::Result whereas most of the time the desire is an ActiveRecord::Relation.
We can achieve this by using Arel to build the where clause like so:
(I modified the example to use description rather than id so that it makes more logical sense)
table = MyObject.arel_table
values = ['Jamesla','Example']
where_clause = values.map {|v| table[:description].matches("%{v}%")}.reduce(&:and)
# OR
where_clause = table[:description].matches_all(values.map {|v| "%#{v}%"})
MyObject.where(where_clause)
This will result in the following SQL query:
SELECT
my_objects.*
FROM
my_objects
WHERE
my_objects.description LIKE '%Jamesla%'
AND my_objects.description LIKE '%Example%'

Related

Create a WHERE (columns) IN (values) clause with Arel?

Is there a way to programatically create a where clause in Arel where the columns and values are specified separately?
SELECT users.*
WHERE (country, occupation) IN (('dk', 'nurse'), ('ch', 'doctor'), ...
Say the input is a really long list of pairs that we want to match.
I'm am NOT asking how to generate a WHERE AND OR clause which is really simple to do with ActiveRecord.
So far I just have basic string manipulation:
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
User.where(
"(#{columns.join(', ')}) IN (#{ pairs.map { '(?, ?)' }.join(', ')})",
*pairs
)
Its not just about the length of the query WHERE (columns) IN (values) will also perform much better on Postgres (and others as well) as it can use an index only scan where OR will cause a bitmap scan.
I'm only looking for answers that can demonstrate generating a WHERE (columns) IN (values) query with Arel. Not anything else.
All the articles I have read about Arel start building of a single column:
arel_table[:foo].eq...
And I have not been able to find any documentation or articles that cover this case.
The trick to this is to build the groupings correctly and then pass them through to the Arel In Node, for example:
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
User.where(
Arel::Nodes::In.new(
Arel::Nodes::Grouping.new( columns.map { |column| User.arel_table[column] } ),
pairs.map { |pair| Arel::Nodes::Grouping.new(
pair.map { |value| Arel::Nodes.build_quoted(value) }
)}
)
)
The above will generate the following SQL statement (for MySQL):
"SELECT users.* FROM users WHERE (users.country,
users.occupation) IN (('dk', 'nurse'), ('ch', 'doctor'))"
This will still generate long query with 'OR' in between. But I felt this is lil elegant/different approach to achieve what you want.
ut = User.arel_table
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
where_condition = pairs.map do |pair|
"(#{ut[columns[0]].eq(pair[0]).and(ut[columns[1]].eq(pair[1])).to_sql})"
end.join(' OR ')
User.where(where_condition)
I have tried this different approach at my end. Hope it will work for you.
class User < ActiveRecord::Base
COLUMNS = %i(
country
occupation
)
PAIRS = [['dk', 'nurse'], ['ch', 'doctor']]
scope :with_country_occupation, -> (pairs = PAIRS, columns = COLUMNS) { where(filter_country_occupation(pairs, columns)) }
def self.filter_country_occupation(pairs, columns)
pairs.each_with_index.reduce(nil) do |query, (pair, index)|
column_check = arel_table[columns[0]].eq(pair[0]).and(arel_table[columns[1]].eq(pair[1]))
if query.nil?
column_check
else
query.or(column_check)
end
end.to_sql
end
end
Call this scope User.with_country_occupation let me know if it works for you.
Thanks!
I think we can do this with Array Conditions as mentioned here
# notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value)
Also, as mentioned here we can use an SQL in statement:
Model.where('id IN (?)', [array of values])
Or simply, as kdeisz pointed out (Using Arel to create the SQL query):
Model.where(id: [array of values])
I have not tried myself, but you can try exploring with these examples.
Always happy to help!

Ruby/Rails - Chain unknown number of method calls

I would like to dynamically create (potentially complex) Active Record queries from a 2D array passed into a method as an argument. In other words, I'd like to take this:
arr = [
['join', :comments],
['where', :author => 'Bob']
]
And create the equivalent of this:
Articles.join(:comments).where(:author => 'Bob')
One way to do this is:
Articles.send(*arr[0]).send(*arr[1])
But what if arr contains 3 nested arrays, or 4, or 5? A very unrefined way would be to do this:
case arr.length
when 1
Articles.send(*arr[0])
when 2
Articles.send(*arr[0]).send(*arr[1])
when 3
Articles.send(*arr[0]).send(*arr[1]).send(*arr[2])
# etc.
end
But is there a cleaner, more succinct way (without having to hit the database multiple times)? Perhaps some way to construct a chain of method calls before executing them?
One convenient way would be to use a hash instead of a 2D array.
Something like this
query = {
join: [:comments],
where: {:author => 'Bob'}
}
This approach is not much complex and You don't need to worry if the key is not provided or is empty
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob'"
If the keys are empty or not present at all
query = {
join: []
}
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles`"
Or nested
query = {
join: [:comments],
where: {:author => 'Bob', comments: {author: 'Joe'}}
}
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob' AND `comments`.`author` = 'Joe'"
I created following query which will work on any model and associated chained query array.
def chain_queries_on(klass, arr)
arr.inject(klass) do |relation, query|
begin
relation.send(query[0], *query[1..-1])
rescue
break;
end
end
end
I tested in local for following test,
arr = [['where', {id: [1,2]}], ['where', {first_name: 'Shobiz'}]]
chain_queries_on(Article, arr)
Query fired is like below to return proper output,
Article Load (0.9ms) SELECT `article`.* FROM `article` WHERE `article`.`id` IN (1, 2) AND `article`.`first_name` = 'Shobiz' ORDER BY created_at desc
Note-1: few noticeable cases
for empty arr, it will return class we passed as first argument in method.
It will return nil in case of error. Error can occur if we use pluck which will return array (output which is not chain-able) or if we do not pass class as first parameter etc.
More modification can be done for improvement in above & avoid edge cases.
Note-2: improvements
You can define this method as a class method for Object class also with one argument (i.e. array) and call directly on class like,
# renamed to make concise
Article.chain_queries(arr)
User.chain_queries(arr)
Inside method, use self instead of klass
arr.inject(Articles){|articles, args| articles.send(*args)}

rails : how can I use find_each with map?

I have a form that has nested field (habtm and accepts_nested_attributes_for). That form contains with a field "keywords", that autocompletes keywords that come from a postgresql table.
All that works well. This is in params :
"acte"=>{"biblio_id"=>"1", "keywords"=>{"keywords"=>"judge, ordeal, "}
What I now need to do is take those keywords and get their keywords_id out of the table keywords. Those id must be added to the join table.
I'm doing this :
q = params[:acte][:keywords].fetch(:keywords).split(",")
a = q.map {|e| Keyword.find_by keyword: e }
As per the guides, find_by returns only the first matching field. I guess I would need to use find_each but I'm not certain about that and I can't get it to word. I have tried this:
q = params[:acte][:motclefs].fetch(:motclefs).split(",")
a = Array.new
Motclef.where(motcle: q).find_each do |mot|
a << mot.id
end
This also finds only the first result like : [251].
What I'm looking to get is something like [1453, 252, 654]
thanks !
Putting find_by in a loop means you will be executing a separate SQL query for each SQL keyword.
You can instead just get all the ids in a single SQL call by doing keyword in.
After you do q = params[:acte][:keywords].fetch(:keywords).split(","), your q will be an array of keywords. So q will be ["judge", " ordeal"].
You can simply do Keyword.where(keyword: q).select(:id) which will generate a query like SELECT keywords.id FROM keywords where keyword in ('judge', 'ordeal').

Rails - How can I avoid using the database while performing a detect method?

When performing detect on a int array, it works:
#number = [1,2,3,4,5,6,7].detect{|n| n == 4}
Variable #number becomes 4.
But when I do something like this:
#categories = Category.all
#current_category = #categories.detect{|cat| cat.id == params[:category]}
The program outputs
Category Load (0.2ms) SELECT "categories".* FROM "categories"
Which means it's using the database to find it.
However, the element I'm trying to find is already in the collection #categories, I just want to find it to assign it to a variable.
Of course another solution would be to implement a linear search algorithm, but I just want to keep the code as clean as possible.
How can I avoid using the database for this search?
EDIT: I just realized that this could be lazy fetching. Because before detect, I never use #categories, so it does the query when I do detect. Could this be true?
Rails is actually performing a SELECT COUNT(*) query when you call #categories.all, essentially performing a lazy-fetch.
Your #categories object still needs to query the database for the data.
See the documentation here: http://apidock.com/rails/ActiveRecord/Scoping/Named/ClassMethods/all
posts = Post.all
posts.size # Fires "select count(*) from posts" and returns the count
posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
fruits = Fruit.all
fruits = fruits.where(color: 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
In your case, you should use active record and SQL requesting.
#current_category = #categories.find_by(id: params[:category])
Using array methods on Active Record relations tend to fetch all the data then apply the algorithm in-memory, while SQL filtering is faster.
In you case I love to define the operator [] on my model:
#in category.rb
def self.[](x)
self.find_by(id: x)
end
# anywhere after:
if c = Category[params[:id]]
puts "Category found and it's #{c.name} !"
else
puts "Not found :("
end

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources