Rails: Multiple or conditions in ActiveRecord - ruby-on-rails

I have two tables: Lawyer and Phone. Phone is separated into area code and number. A lawyer has many phones. I want to produce a query that searches for lawyers who have a phone matching a phone from a list of phones.
If I had only one phone I could search it like this:
Lawyer.join(:phones).where(:area_code => area_code, :number => number)
The problem is that I have a list with more than one area code. So I really want to do something like this:
lawyers = []
phones.each { |phone| lawyers += Lawyer.join(:phones).where(:area_code => phone[:area_code], :number => phone[:number]) }
However, I don't want to make many queries. Can't it be done in a single query statement?
Edit: This is how I would do a similar thing using SQL alone (assuming the list of numbers was [{:area_code=>'555', :number=>'1234564'}, {:area_code=>'533', :number=>'12345678'}])
select * from phones where (area_code, number) in (('555', '1234564'), ('533', '12345678'))
If someone can get that translated into ActiveRecord, that'd be great.

If you pass an array of area_codes, AR will build an IN condition. So you could use your original search and use arrays instead:
where area_codes is an array of area_codes and numbers an array of numbers:
Lawyer.join(:phones).where(:area_code => area_codes, :number => numbers)
or:
Lawyer.join(:phones).where("phones.area_code IN (?) AND phones.number IN (?)", area_codes, numbers)
Should yield:
SELECT * from lawyers JOIN phones ON phones.lawyer_id = lawyers.id WHERE phones.area_code IN (...) AND phones.number IN (...)

Lawyer.join(:phones).where(
"(phones.area_code, phones.number) IN ( ('555', '5555555'), ('444', '12345678') )"
)

Related

How to get a most recent value group by year by using SQL

I have a Company model that has_many Statement.
class Company < ActiveRecord::Base
has_many :statements
end
I want to get statements that have most latest date field grouped by fiscal_year_end field.
I implemented the function like this:
c = Company.first
c.statements.to_a.group_by{|s| s.fiscal_year_end }.map{|k,v| v.max_by(&:date) }
It works ok, but if possible I want to use ActiveRecord query(SQL), so that I don't need to load unnecessary instance to memory.
How can I write it by using SQL?
select t.username, t.date, t.value
from MyTable t
inner join (
select username, max(date) as MaxDate
from MyTable
group by username
) tm on t.username = tm.username and t.date = tm.MaxDate
For these kinds of things, I find it helpful to get the raw SQL working first, and then translate it into ActiveRecord afterwards. It sounds like a textbook case of GROUP BY:
SELECT fiscal_year_end, MAX(date) AS max_date
FROM statements
WHERE company_id = 1
GROUP BY fiscal_year_end
Now you can express that in ActiveRecord like so:
c = Company.first
c.statements.
group(:fiscal_year_end).
order(nil). # might not be necessary, depending on your association and Rails version
select("fiscal_year_end, MAX(date) AS max_date")
The reason for order(nil) is to prevent ActiveRecord from adding ORDER BY id to the query. Rails 4+ does this automatically. Since you aren't grouping by id, it will cause the error you're seeing. You could also order(:fiscal_year_end) if that is what you want.
That will give you a bunch of Statement objects. They will be read-only, and every attribute will be nil except for fiscal_year_end and the magically-present new field max_date. These instances don't represent specific statements, but statement "groups" from your query. So you can do something like this:
- #statements_by_fiscal_year_end.each do |s|
%tr
%td= s.fiscal_year_end
%td= s.max_date
Note there is no n+1 query problem here, because you fetched everything you need in one query.
If you decide that you need more than just the max date, e.g. you want the whole statement with the latest date, then you should look at your options for the greatest n per group problem. For raw SQL I like LATERAL JOIN, but the easiest approach to use with ActiveRecord is DISTINCT ON.
Oh one more tip: For debugging weird errors, I find it helpful to confirm what SQL ActiveRecord is trying to use. You can use to_sql to get that:
c = Company.first
puts c.statements.
group(:fiscal_year_end).
select("fiscal_year_end, MAX(date) AS max_date").
to_sql
In that example, I'm leaving off order(nil) so you can see that ActiveRecord is adding an ORDER BY clause you don't want.
for example you want to get all statements by start of the months you should use this
#companey = Company.first
#statements = #companey.statements.find(:all, :order => 'due_at, id', :limit => 50)
then group them as you want
#monthly_statements = #statements.group_by { |statement| t.due_at.beginning_of_month }
Building upon Bharat's answer you can do this type of query in Rails using find_by_sql in this way:
Statement.find_by_sql ["Select t.* from statements t INNER JOIN (
SELECT fiscal_year_end, max(date) as MaxDate GROUP BY fiscal_year_end
) tm on t.fiscal_year_end = tm.fiscal_year_end AND
t.created_at = tm.MaxDate WHERE t.company_id = ?", company.id]
Note the last where part to make sure the statements belong to a specific company instance, and that this is called from the class. I haven't tested this with the array form, but I believe you can turn this into a scope and use it like this:
# In Statement model
scope :latest_from_fiscal_year, lambda |enterprise_id| {
find_by_sql[..., enterprise_id] # Query above
}
# Wherever you need these statements for a particular company
company = Company.find(params[:id])
latest_statements = Statement.latest_from_fiscal_year(company.id)
Note that if you somehow need all the latest statements for all companies then this most likely leave you with a N+1 queries problem. But that is a beast for another day.
Note: If anyone else has a way to have this query work on the association without using the last where part (company.statements.latest_from_year and such) let me know and I'll edit this, in my case in rails 3 it just pulled em from the whole table without filtering.

Querying based on two associated records

I have a product that has many variants, those variants have two attributes: Size and Color.
I want to query for the Variant based on the two attributes I pass in - I got it to work with following:
variants = Spree::Variant.joins(:option_values).where(:spree_option_values => {:id => size.id}, :product_id => prod.id).joins(:option_values)
variant = variants.select{|v| v.option_values.include?(size)}
From my understanding, the select method more or less iterates through the array, which is kinda slow. I would rather have a query that finds the variant directly based on those two attributes.
I tried the following:
Spree::Variant.joins(:option_values).where(:spree_option_values => {:id => size.id}, :product_id => prod.id).joins(:option_values).where(:spree_option_values => {:id => color.id})
but this only ended up in returning an empty array.
How would I go about this?
Edit: Here are the product, variant and option_values models:
Product:
https://github.com/spree/spree/blob/master/core/app/models/spree/product.rb
Variant:
https://github.com/spree/spree/blob/master/core/app/models/spree/variant.rb
OptionValue: https://github.com/spree/spree/blob/master/core/app/models/spree/option_value.rb
OptionType: https://github.com/spree/spree/blob/master/core/app/models/spree/option_type.rb
Updated 2: you're right, this is not what you looking for.
So you can:
1) Build SQL subquery: (if joined table has size and has color at the same time then return TRUE). How quick it will be working - is a question...
2) Imagine you've created a model "ValuesVariants" for table "spree_option_values_variants" and kicked out habtm (replace with 2 has_manys + 2 has_manys through). Now you can search ValuesVariants with (option_type_id = size_id||color_id AND variant_id IN (array of product's variant ids)), extracting matched variants. It can be quick enough...
3) You can use :includes. so associated objects loaded into the memory and the second search do by array methods. In this case the concern is in memory usage.

How do I combine ActiveRecord results from multiple has_many :through queries?

Basically, I have an app with a tagging system and when someone searches for tag 'badger', I want it to return records tagged "badger", "Badger" and "Badgers".
With a single tag I can do this to get the records:
#notes = Tag.find_by_name(params[:tag_name]).notes.order("created_at DESC")
and it works fine. However if I get multiple tags (this is just for upper and lower case - I haven't figured out the 's' bit either yet):
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger'])
I can't use .notes.order("created_at DESC") because there are multiple results.
So, the question is.... 1) Am I going about this the right way? 2) If so, how do I get all my records back in order?
Any help much appreciated!
One implementation would be to do:
#notes = []
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger']).each do |tag|
#notes << tag.notes
end
#notes.sort_by {|note| note.created_at}
However you should be aware that this is what is known as an N + 1 query, in that it makes one query in the outer section, and then one query per result. This can be optimized by changing the first query to be:
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger'], :includes => :notes).each do |tag|
If you are using Rails 3 or above, it can be re-written slightly:
Tag.where("lower(name) = ?", "badger").includes(:notes) do |tag|
Edited
First, get an array of all possible tag names, plural, singular, lower, and upper
tag_name = params[:tag_name].to_s.downcase
possible_tag_names = [tag_name, tag_name.pluralize, tag_name.singularize].uniq
# It's probably faster to search for both lower and capitalized tags than to use the db's `lower` function
possible_tag_names += possible_tag_names.map(&:capitalize)
Are you using a tagging library? I know that some provide a method for querying multiple tags. If you aren't using one of those, you'll need to do some manual SQL joins in your query (assuming you're using a relational db like MySQL, Postgres or SQLite). I'd be happy to assist with that, but I don't know your schema.

Rails 3 select random follower query efficiency

I have a method that selects 5 random users who are following a certain user, and adds them to an array.
Relationship.find_all_by_followee_id( user.id ).shuffle[0,4].each do |follower|
follower = User.find(follower.user_id)
array.push follower
end
return array
I'm wondering, is this an efficient way of accomplishing this? My main concern is with the find_all_by_followee_id call. This returns a list of all the relationships where the specified user is being followed (this could be in the 100,000s). And then I shuffle that entire list, and then I trim it to the first 5. Is there a more efficient way to do this?
You can try this:
Relationship.find_all_by_followee_id( user.id, :order => 'rand()', :limit => 5 ) do |follower|
follower = User.find(follower.user_id)
array.push follower
end
return array
Btw, this will work with MySql. If you are using PostgreSQL or anything else you may need to change the rand() with any valid random function that your DB supports.
Some minor changes to make it a little more clean:
return Relationship.find_all_by_followee_id( user.id, :order => 'rand()', :limit => 5 ).collect {|follower| User.find(follower.user_id) }
You can also use a join in there in order to prevent the 5 selects but it won't make much difference.
Edit1:
As #mike.surowiec mentioned.
"Just for everyones benefit, translating this to the non-deprecated active record query syntax looks like this:"
Relationship.where(:followee_id => user.id).order( "random()" ).limit( 5 ).collect {|follower| User.find(follower.user_id) }

rails 3, active record: any way to tell how many unique values match a "x LIKE ?" query

I have a query to find all the phone numbers that match a partial expression such as "ends with 234"
#matchingphones = Calls.find :all,
:conditions => [ "(thephonenumber LIKE ?)", "%234"]
The same phone number might be in the database several times, and so might be returned multiple times by this query if it matches.
What I need is to know is UNIQUE phone numbers the query returns.
For example if the database contains
000-111-1234 *
000-111-3333
000-111-2234 *
000-111-1234 *
000-111-4444
the existing query will return the 3 records marked with * (eg returns one phone number -1234 twice since it's in the database twice)
what I need is a query that returns just once instance of each match, in this case
000-111-1234 *
000-111-2234 *
Calls.find :all,
:select => "id, DISTINCT thephonenumber",
:conditions => [ "(thephonenumber LIKE ?)", "%234"]
In addition,
1. You are using rails 2 query syntax.. better switch to rails 3 (arel)
2. Better name your class Call (not Calls)
Call.where("thephonenumber LIKE ?", "%234").group(:thephonenumber)
if you just want the count then you can do:
Call.where("thephonenumber LIKE ?", "%234").group(:thephonenumber).all.count

Resources