Create a WHERE (columns) IN (values) clause with Arel? - ruby-on-rails

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!

Related

how to match all multiple conditions with rails where method

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%'

How to get a unique set of parent models after querying on child

Order has_many Items is the relationship.
So let's say I have something like the following 2 orders with items in the database:
Order1 {email: alpha#example.com, items_attributes:
[{name: "apple"},
{name: "peach"}]
}
Order2 {email: beta#example.com, items_attributes:
[{name: "apple"},
{name: "apple"}]
}
I'm running queries for Order based on child attributes. So let's say I want the emails of all the orders where they have an Item that's an apple. If I set up the query as so:
orders = Order.joins(:items).where(items: {name:"apple"})
Then the result, because it's pulling at the Item level, will be such that:
orders.count = 3
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com", "beta#example.com"]
But my desired outcome is actually to know what unique orders there are (I don't care that beta#example.com has 2 apples, only that they have at least 1), so something like:
orders.count = 2
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
How do I do this?
If I do orders.select(:id).distinct, this will fix the problem such that orders.count == 2, BUT this distorts the result (no longer creates AR objects), so that I can't iterate over it. So the below is fine
deduped_orders = orders.select(:id).distinct
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
But then the below does NOT work:
deduped_orders.each do |o|
puts o.email # ActiveModel::MissingAttributeError: missing attribute: email
end
Like I basically want the output of orders, but in a unique way.
I find using subqueries instead of joins a bit cleaner for this sort of thing:
Order.where(id: Item.select(:order_id).where(name: 'apple'))
that ends up with this (more or less) SQL:
select *
from orders
where id in (
select order_id
from items
where name = 'apple'
)
and the in (...) will clear up duplicates for you. Using a subquery also clearly expresses what you want to do–you want the orders that have an item named 'apple'–and the query says exactly that.
use .uniq instead of .distinct
deduped_orders = orders.select(:id).uniq
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
If you want to keep all the attributes of orders use group
deduped_orders = orders.group(:id).distinct
deduped_orders.each do |o|
puts o.email
end
#=> output: "alpha#exmaple.com", "beta#example.com"
I think you just need to remove select(:id)
orders = Order.joins(:items).where(items: {name:"apple"}).distinct
orders.pluck(:email)
# => ["alpha#exmaple.com", "beta#example.com"]
orders = deduped_orders
deduped_orders.each do |o|
puts o.email # loop twice
end

Postgresql syntax for select a record (row) with two of three fields

I have a table QualifyingEvents. I am attempting to search the table for records created on a particular date with their course_id and / or their sponsor_name. I'm following the examples in RailsGuides Active Record Query Interface: http://guides.rubyonrails.org/active_record_querying.html
I couldn't get a query of this form to work:
QualifyingEvent.find([:date = 'x', :course_id = 'x', :sponsor_name ='x'])
Nor this form:
QualifyingEvent.where(":course_id = ?", x )
However, this works - with the commas being interpreted as "and":
QualifyingEvent.find_by date: '2014', course_id: '96789', sponsor_name: 'Maggio and Sons'
Now, how do I get a record if I want a match on two of the three fields? I've tried using && and || with no luck. What is the correct syntax? Also, why does "find_by" work when "find" doesn't?
Thank you, in advance, for your help.
To match just two fields, one way is:
QualifyingEvent.where(field1: field1_val, field2: field2_val).first
OR
QualifyingEvent.find_by(field1: field1_val, field2: field2_val)
That hash provided as an argument to where and find_by can be passed in different forms e.g.
"field1 = ? AND field2 = ?", field1_val, field2_val
OR
"field1 = :f1 AND field2 = :f2", {f1: field1_val, f2: field2_val}
OR
"field1 = #{field1_val} AND field2 = #{field2_val}"
That last one isn't advised though mainly because of the risk of SQL injection.
But if you meant matching any two of the three field values you gave, then you'll have to provide all three match-scenarios yourself and specify the "OR" between them.
QualifyingEvent.where("
(date = :date AND course_id = :course_id)
OR
(date = :date AND sponsor_name = :sponsor_name)
OR
(course_id = :course_id AND sponsor_name = :sponsor_name)
",
{date: date_val, course_id: course_val, sponsor_name: sponsor_val}
).first
Again, the above code can be written in different ways.
And find_by is for certain conditions that you can pass as a hash whereas find is for finding by ID or array of IDs. Shortcut for find_by(id: some_value) i.e.
find_by(id: 1) == find(1)
Try something like this:
QualifyingEvent.where("course_id = ? and some_other_col = ? and some_more_col = ?", x,y,z)
Or
QualifyingEvent.where(:course_id => 'x', :some_other_col => 'y', :some_more_col => 'z')
Any one of them will fetch you the result.

rails how to find with no associated records

I know that this will be an easy one but I'm having real issues working it out.
I have users that can have_many results. I'm trying to work out how to return users that don't yet have any results to the #starters object(from the controller).
#users = #event.entires
#starters = #users.where("results = ?", 0)
could anyone explain how i would check if a user has no results?
Best solution (as MrYoshiji commented)
#starters = #users.includes(:results).where(results: { id: nil })
This will execute the same query as the one in my second solution.
Other SQL solution
You could use a LEFT OUTER JOIN. This way, you will always have all the results from the "left" table (users) but you will also have matching records for the "right" table (results) eventhough there are non, which will leave you with empty fields that you can check.
#starters = #users.joins("LEFT OUTER JOIN results ON results.user_id = users.id").where("results.user_id IS NULL")
In your case, replace users with the name of your "user" model.
Other Ruby solution
#starters = #users.select { |u| !!u.results }
Here !! will force conversion to a boolean, if there are no results, u.results will return [] (empty array). And !![] equals true.
Try left joining and finding which one is null
#users.joins("LEFT OUTER JOIN user ON user.id = result.user_id").where("result.id IS NULL")
If your #users is an Array, try:
#starters = #users.select { |u| u.results.empty? }
This should get all of the users that do not have any results:
#starters = ActiveRecord::Base.execute("select * from users where id not in (select user_id from results)")
Another way to do this would be the following:
User.where("id not in ?", "(select user_id from results)")

How to combine ActiveRecord objects?

I'm wondering if there's an efficient way to combine the results of multiple ActiveRecord objects in Rails. For example, I might make three individual calls to three individual tables, and I want the results combined, and sorted by a common column.
Here's a super basic code example that will hopefully make my question easier to understand:
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
#combined_results_sorted_by_date_column = (how?)
As suggested by others, here's one solution to the problem.
#combined_results = #result1 + #result2 + #result3
#combined_results.sort! {|x,y| x.date <=> y.date}
What if I want to sort by date, but Table3 refers to the "created_on" column as date?
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
#combined_results_sorted_by_date_column =
(#results1 + #results2 + #results3).sort_by(&:date)
What if I want to sort by date, but Table3 refers to the "created_on" column as date?
class Table3
alias_method :date, :created_on
end
or simply
class Table3
alias date created_on
end
You don't work with "Tables" but rather objects.
If you think about it this way, it would make no sense to have:
#results1 = Users.find(:all)
#results2 = Posts.find(:all)
#results3 = Comments.find(:all)
What would the "combined" form of it means?
What you probably want is to combine results from the same kind using different "queries".
Is that it?
You're probably not going to like this answer, but I would say you might want to revise your database schema. I was in a similar situation, and sorting the results after concatenating them is definitely not the way you want to go.
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
combined = (#results1 + #results2 + #results3).sort { |x, y| x.date <=> y.date }
#combined_results = #result1 + #result2 + #result3
#combined_results.sort! {|x,y| x.date <=> y.date}
While this surely is not be the most efficient code in the world, it might just be what you need.
If some models don't have a date method I suggest you create one.
It is as easy as.
def date
created_on
end
I'm assuming you want a mixed array of three different types of ActiveRecord objects, sorted by a date of some kind.
#array = (Bucket.all + Mop.all + Detergent.all).sort{|x,y| x.sort_by <==> y.sort_by}
Since your sort_by field is different for each object type, you need to define it.
class Bucket < ActiverRecord::Base
def sort_by
cleaned_on
end
end
class Detergent < ActiverRecord::Base
def sort_by
purchased_on
end
end
You could pull in all the data sorted in a single query using UNION, but you wouldn't get AR objects out of that.
I'm not sure what your data volumes are like, but when it comes to sorting, your database will do a better job of it than you ever will using "application layer" code.
Do you need the data returned as an array of model objects seeing as the three tables will probably generate a mixed up array of three distinct model classes ?
Why not use direct-SQL returning rows and columns and have the DB do all the hard work sorting the data ?

Resources