ActiveRecord vs SQL with sub select - ruby-on-rails

I found this query easier if done with SQL
select Topics.subject, shortcode, (select count(*) from votes where Votes.Topic_Id = Topics.Id ) as votes from Topics
where url like 'http://test.com%'
ORDER BY votes desc;
Using ActiveRecord, I think there should be a more elegant.. or at least possible way to do it. Any suggestions?
I started with this, which worked, but didn't get to the next steps, instead used:
t = Topic.find(:all, :conditions => "url like 'http://test.com%'")

To get topics with votes:
Topic.where('url like :url', :url => 'http://test.com%').
joins(:votes).
select('topics.*, count(votes.id) as votes')
Note that this will only work in MySql. For PostgreSQL, you need to specify the group clause:
Topic.where('url like :url', :url => 'http://test.com%').
joins(:votes).
group(Topic.column_names.map{|col| "topics.#{col}"}).
select('topics.*, count(votes.id) as votes')

Related

Rails "nested" join query

In my database, I have the following tables and relationships:
Inspections --has_many--< Samples --has_many--< Results
A sample is considered 'analyzed' if it has one or more results associated to it. An inspection is considered 'complete' if all its samples are analyzed. I need to find all 'incomplete' inspections; that is, all inspections that have at least one sample that has not been analyzed.
My query to do this in the mysql database is
SELECT DISTINCT inspections.*
FROM inspections
JOIN samples s ON inspections.id = s.inspection_id
LEFT OUTER JOIN results r ON r.`sample_id` = s.`id`
WHERE r.id IS NULL
I'm trying to turn this in to a nice ActiveRecord find call (aside from find_by_sql), but I'm not sure how to get that left join of the "nested" association (terminology?) into the syntax.
Can anyone help me out? BTW, this is for a Rails 2.3 app.
For now I have
Inspection.all(:select => "distinct inspections.*",
:joins => "join samples on samples.inspection_id = inspections.id " +
"left join results on results.sample_id = samples.id",
:conditions => "results.id is null")
It works, but still looks unrefined and too close to the entire sql statement. Is there something a little cleaner than this?
Use :include in the find statement:
Inspection.find(:all, :include => {:samples => :results})
Edit:
I missed the combination of INNER JOIN and LEFT JOIN before. My apologies.
While it doesn't generate the same query, you could add an additional where check to filter out the inspections that don't have any samples.
Inspection.find(:all,
:select => "DISTINCT inspections.*",
:include => {:samples => :results},
:conditions => "results.id IS NULL AND samples.id IS NOT NULL")
Note, however, that is approach won't perform as well as doing it with JOINs.

left join not providing the object properties i want

I'm sure I'm doing something stupid, but my AR code
#comments = Comment.find(:all, :limit => 10,
:joins => "LEFT JOIN `users` ON comments.user_id = users.id",
:select => 'comments.*, users.theme')
Is returning the correct sql:
SELECT comments.*, users.theme from comments LEFT JOIN users on comments.user_id = users.id
and when I put it into a mysql, I get the results I want, but when I try to access #comment.theme (in an each loop on #comments) after the above AR call is made, theme is not there.
So, is there something special I have to do to the Comments model to allow joins to populate the associated columns? I thought that Rails would just add them as properties I could dot-grab.
Try this:
#comments.each do |comment|
puts comment.theme
end
All the fields are in the array #comments, as Object type Comment, and the field names are as used in the select, with no regard to their original source.
Good luck.

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

Elegant Summing/Grouping/Etc in Rails

I have a number of objects which are associated together, and I'd like to layout some dashboards to show them off. For the sake of argument:
Publishing House - has many books
Book - has one author and is from one, and goes through many states
Publishing House Author - Wrote many
books
I'd like to get a dashboard that said:
How many books a publishing house put
out this month?
How many books an
author wrote this month?
What state (in progress, published) each of the books are in?
To start with, I'm thinking some very simple code:
#all_books = Books.find(:all, :joins => [:author, :publishing_house], :select => "books.*, authors.name, publishing_houses.name", :conditions => ["books.created_at > ?", #date])
Then I proceed to go through each of the sub elements I want and total them up into new arrays - like:
#ph_stats = {}
#all_books.map {|book| #ph_stats[book.publishing_house_id] = (#ph_stats[book.publishing_house_id] || 0) + 1 }
This doesn't feel very rails like - thoughts?
I think your best bet is to chain named scopes together so you can do things like:
#books = Books.published.this_month
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html#M001683
http://m.onkey.org/2010/1/22/active-record-query-interface
You should really be thinking of the SQL required to write such a query, as such, the following queries should work in all databases:
Number of books by publishing house
PublishingHouse.all(:joins => :book, :select => "books.publishing_house_id, publishing_houses.name, count(*) as total", :group => "1,2")
Number of books an author wrote this month
If you are going to move this into a scope - you WILL need to put this in a lambda
Author.all(:joins => :books, :select => "books.author_id, author.name, count(*) as total", :group => "1,2", :conditions => ["books.pub_date between ? and ?", Date.today.beginning_of_month, Date.today.end_of_month])
this is due to the use of Date.today, alternatively - you could use now()::date (postgres specific) and construct dates based on that.
Books of a particular state
Not quite sure this is right wrt your datamodel
Book.all(:joins => :state, :select => "states.name, count(*) as total", :group => "1")
All done through the magic of SQL.

Find all objects with no associated has_many objects

In my online store, an order is ready to ship if it in the "authorized" state and doesn't already have any associated shipments. Right now I'm doing this:
class Order < ActiveRecord::Base
has_many :shipments, :dependent => :destroy
def self.ready_to_ship
unshipped_orders = Array.new
Order.all(:conditions => 'state = "authorized"', :include => :shipments).each do |o|
unshipped_orders << o if o.shipments.empty?
end
unshipped_orders
end
end
Is there a better way?
In Rails 3 using AREL
Order.includes('shipments').where(['orders.state = ?', 'authorized']).where('shipments.id IS NULL')
You can also query on the association using the normal find syntax:
Order.find(:all, :include => "shipments", :conditions => ["orders.state = ? AND shipments.id IS NULL", "authorized"])
One option is to put a shipment_count on Order, where it will be automatically updated with the number of shipments you attach to it. Then you just
Order.all(:conditions => [:state => "authorized", :shipment_count => 0])
Alternatively, you can get your hands dirty with some SQL:
Order.find_by_sql("SELECT * FROM
(SELECT orders.*, count(shipments) AS shipment_count FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' GROUP BY orders.id)
AS order WHERE shipment_count = 0")
Test that prior to using it, as SQL isn't exactly my bag, but I think it's close to right. I got it to work for similar arrangements of objects on my production DB, which is MySQL.
Note that if you don't have an index on orders.status I'd strongly advise it!
What the query does: the subquery grabs all the order counts for all orders which are in authorized status. The outer query filters that list down to only the ones which have shipment counts equal to zero.
There's probably another way you could do it, a little counterintuitively:
"SELECT DISTINCT orders.* FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' AND shipments.id IS NULL"
Grab all orders which are authorized and don't have an entry in the shipments table ;)
This is going to work just fine if you're using Rails 6.1 or newer:
Order.where(state: 'authorized').where.missing(:shipments)

Resources