Complex rails find ordering - sql

I am trying to do a find which orders results by their house name and then by the customer's last name.
Customer.find(:all,
:conditions =>['customers.id IN (?)', intersection],
:joins => 'JOIN histories ON histories.customer_id = customers.id
JOIN houses ON histories.house_id = houses.id',
:order => "houses.name ASC, customers.last_name ASC",
:select => "customers.*, histories.job_title, houses.name"
)
My problem is this will return every history related to each customer.
if I add AND histories.finish_date IS NULL
This will prevent every history for the selected customer being returned but it will also stop customers in the intersection who have no history or a finish_date set from being returned.
Basically I need every customer in the intersection returned once with there current house name(if they have one) and then ordered by their house name and then their last name.
So is there a way of doing this?
Here is an example
customer
id last_name
1 franks
2 doors
3 greens
histories
id finish_date house_id customer_id
1 NULL 1 1
2 NULL 2 2
3 11/03/10 2 1
4 22/04/09 1 2
NULL = current house
houses
id name
1 a
2 b
Results
intersection = 1,2,3
last_name house
franks a
doors b
greens NULL
Thanks

I think you need to use outer joins.
For example, this should work:
Customer.find(:all,
:conditions =>['customers.id IN (?) and histories.finish_date is null', intersection],
:joins => 'LEFT OUTER JOIN histories ON histories.customer_id = customers.id
LEFT OUTER JOIN houses ON histories.house_id = houses.id',
:order => "houses.name ASC, customers.last_name ASC",
:select => "customers.*, histories.job_title, houses.name"
)
If you've got an association between Customer and History and between History and House you should be able to do :include => [:histories => :house] instead of the :joins option.
The only other thing is that the customers with no house will appear first in the list due to NULL being earlier in the order than a non-NULL value. You might want to try an order option like this :
:order => 'isnull(houses.name), houses.name, customers.last_name'
to achieve what you specified.

IMO it's simpler to do the sorting logic in Rails instead of the database:
customers = Customer.find(:all, :conditions => { :id => intersection }, :include => [ { :histories => :houses } ])
customers.sort_by { |c| c.last_name }
customers.sort_by do |c|
current_house = c.histories.find_by_finish_date(nil) # Returns nil if no matching record found
if current_house
current_house.name
else
''
end
end
Explanations
:conditions can take an hash { :column_name => array } which translates into your IN where-condition
:include pre-loads (eager loading) the tables if the corresponding associations exist. To put it another way: :joins creates INNER JOINs, while :include creates LEFT JOINs. Here we will left join histories and again left join houses. You could omit this :include tag, in which case rails does a new query each time you access a histories or houses property.
sort_by allows to define a custom sort criteria.
find_by_finish_date is one of rails' magic methods; it is equivalent to h.find(:conditions => {:finish_date => nil })
How to output: Just output all of them in your view. If he does not have histories, customer.histories is an empty array.

Related

Is it the right way to do union query after joins in rails 3.2?

There are 3 models log (which belongs to customer), customer and project in rails 3.2 app. Both customer and project have sales_id field. Here is the query we want to do:
return the following logs for customers 1) logs for customers whose sales_id is equal to session[:user_id] and 2) logs for customers whose projects' sales_id is equal to session[:user_id]
The rails query for 1) could be:
Log.joins(:customer).where(:customers => {:sales_id => session[:user_id]})
Rails query for 2) could be:
Log.joins(:customer => :projects).where(:projects => {:sales_id => session[:user_id})
To combine the queries above, is it the right way to do the following?
Log.joins([:customer, {:customer => :projects}]).where('customers.sales_id = id OR projects.sales_id = id', id: session[:user_id])
Chapter 11.2.4 in http://guides.rubyonrails.org/v3.2.13/active_record_querying.html talks about an interesting query case. We haven't tested the query above yet. We would like to know if the union query above is indeed correct.
Rails doesn't support union natively. In your case, I think it doesn't need union, just use left outer join.
Log.joins('left outer JOIN `customers` ON `customers`.`id` = `logs`.`customer_id`
left outer JOIN `projects` ON `projects`.`customer_id` = `customers`.`id`').where('customers.sales_id = :id OR projects.sales_id = :id', id: session[:user_id]).distinct

Can I :select multiple fields (*, foo) without the extra ones being added to my instances (Instance.foo=>bar)

I'm trying to write a named scope that will order my 'Products' class based on the average 'Review' value. The basic model looks like this
Product < ActiveRecord::Base
has_many :reviews
Review < ActiveRecord::Base
belongs_to :product
# integer value
I've defined the following named scope on Product:
named_scope :best_reviews,
:select => "*, AVG(reviews.value) score",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "score desc"
This seems to be working properly, except that it's adding the 'score' value in the select to my Product instances, which causes problems if I try to save them, and makes comparisons return false (#BestProduct != Product.best_reviews.first, becuase Product.best_reviews.first has score=whatever).
Is there a better way to structure the named_scope? Or a way to make Rails ignore the extra field in the select?
I'm not a Rails developer, but I know SQL allows you to sort by a field that is not in the select-list.
Can you do this:
:select => "*",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "AVG(reviews.value) desc"
Wow, so I should really wait before asking questions. Here's one solution (I'd love to hear if there are better approaches):
I moved the score field into the inner join. That makes it available for ordering but doesn't seem to add it to the instance:
named_scope :best_reviews,
:joins => "INNER JOIN (
SELECT *, AVG(value) score FROM reviews GROUP BY reviews.product_id
) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "reviews.score desc"

Complex Join Queries in Rails

I have 3 tables - venues, users, and updates (which have a integer for rating) - and I want to write a query that will return a list of all my venues as well as their average ratings using only the most recent update for each person, venue pair. For example, if user 1 rates venue A once at 9 am with a 4, and then rates it again at 5 pm with a 3, I only want to use the rating of 3, since it's more recent. There are also some optional conditions, such as how recent the updates must be, and if there is an array of user ids the users must be within.
Does anybody have a suggestion on what the best way to write something like this is so that it is clean and efficient? I have written the following named_scope which should do the trick, but it is pretty ugly:
named_scope :with_avg_ratings, lambda { |*params|
hash = params.first || {}
has_params = hash[:user_ids] || hash[:time_ago]
dir = hash[:dir] || 'DESC'
{
:joins => %Q{
LEFT JOIN (select user_id, venue_id, max(updated_at) as last_updated_at from updates
WHERE type = 'Review' GROUP BY user_id, venue_id) lu ON lu.venue_id = venues.id
LEFT JOIN updates ON lu.last_updated_at = updates.updated_at
AND updates.venue_id = venues.id AND updates.user_id = lu.user_id
},
:select => "venues.*, ifnull(avg(rating),0) as avg_rating",
:group => "venues.id",
:order => "avg_rating #{dir}",
:conditions => Condition.block { |c|
c.or { |a|
a.and "updates.user_id", hash[:user_ids] if hash[:user_ids]
a.and "updates.updated_at", '>', hash[:time_ago] if hash[:time_ago]
} if has_params
c.or "updates.id", 'is', nil if has_params
}
}
}
I include the last "updates.id is null" condition because I still want the venues returned even if they don't have any updates associated with them.
Thanks,
Eric
Yikes, that looks like a job for find_by_sql to me. When you're doing something that complex, I find it's best to take the job away from ActiveRecord and DIY.

Pull back rows from multiple tables with a sub-select?

I have a script which generates queries in the following fashion (based on user input):
SELECT * FROM articles
WHERE (articles.skeywords_auto ilike '%pm2%')
AND spubid IN (
SELECT people.spubid FROM people
WHERE (people.slast ilike 'chow')
GROUP BY people.spubid)
LIMIT 1;
The resulting data set:
Array ( [0] =>
Array (
[spubid] => A00603
[bactive] => t
[bbatch_import] => t
[bincomplete] => t
[scitation_vis] => I,X
[dentered] => 2009-07-24 17:07:27.241975
[sentered_by] => pubs_batchadd.php
[drev] => 2009-07-24 17:07:27.241975
[srev_by] => pubs_batchadd.php
[bpeer_reviewed] => t
[sarticle] => Errata: PM2.5 and PM10 concentrations from the Qalabotjha low-smoke fuels macro-scale experiment in South Africa (vol 69, pg 1, 2001)
[spublication] => Environmental Monitoring and Assessment
[ipublisher] =>
[svolume] => 71
[sissue] =>
[spage_start] => 207
[spage_end] => 210
[bon_cover] => f
[scover_location] =>
[scover_vis] => I,X
[sabstract] =>
[sabstract_vis] => I,X
[sarticle_url] =>
[sdoi] =>
[sfile_location] =>
[sfile_name] =>
[sfile_vis] => I
[sscience_codes] =>
[skeywords_manual] =>
[skeywords_auto] => 1,5,69,2001,africa,assessment,concentrations,environmental,errata,experiment,fuels,low-smoke,macro-scale,monitoring,pg,pm10,pm2,qalabotjha,south,vol
[saward_number] =>
[snotes] =>
)
The problem is that I also need all the columns from the 'people' table (as referenced in the sub select) to come back as part of the data set. I haven't (obviously) done much with sub selects in the past so this approach is very new to me. How do I pull back all the matching rows/columns from the articles table AS WELL as the rows/column from the people table?
Are you familiar with joins? Using ANSI syntax:
SELECT DISTINCT *
FROM ARTICLES t
JOIN PEOPLE p ON p.spubid = t.spudid AND p.slast ILIKE 'chow'
WHERE t.skeywords_auto ILIKE'%pm2%'
LIMIT 1;
The DISTINCT saves from having to define a GROUP BY for every column returned from both tables. I included it because you had the GROUP BY on your subquery; I don't know if it was actually necessary.
Could you not use a join instead of a sub-select in this case?
SELECT a.*, p.*
FROM articles as a
INNER JOIN people as p ON a.spubid = p.spubid
WHERE a.skeywords_auto ilike '%pm2%'
AND p.slast ilike 'chow'
LIMIT 1;
Lets start from the beginning
You shouldn't need a group by. Use distinct instead (you aren't doing any aggregating in the inner query).
To see the contents of the inner table, you actually have to join it. The contents are not exposed unless it shows up in the from section. A left outer join from the people table to the articles table should be equivalent to an IN query :
SELECT *
FROM people
LEFT OUTER JOIN articles ON articles.spubid = people.spubid
WHERE people.slast ilike 'chow'
AND articles.skeywords_auto ilike '%pm2%'
LIMIT 1

Rails combined ('AND') searches on associated join tables

I cant get rails to return combined ('AND') searches on associated join tables of an Object.
E.g. I have Books that are in Categories. Lets say: Book 1: is in category 5 and 8
But I can't get 'AND' to filter results using the join table? E.g ::->
Class Books
has_and_belongs_to_many :categories, :join_table => "book_categories"
Book.find :all, :conditions => "book_categories.category_id = 5 AND book_categories.category_id = 8", :include => "categories"
... returns nil
(why does it not return all books that are in both 5 & 8 ??)
However: 'OR' does work:
Book.find :all, :conditions => "book_categories.category_id = 5 OR book_categories.category_id = 8"
... returns all books in category 5 and 8
I must be missing something?
The problem is at the SQL level. That condition runs on a link table row, and any individual link table row can never have a category_id of both 5 and 8. You really want separate link table rows to have these IDs.
Try looking into Rails' named_scope, specifically the part that allows filtering with a lambda (so you can take an argument). I've never tried it out myself, but if I had to implement what you're looking for, that's what I'd look in to.