Rails "nested" join query - ruby-on-rails

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.

Related

ActiveRecord vs SQL with sub select

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

Is there an idiomatic way to cut out the middle-man in a join in Rails?

We have a Customer model, which has a lot of has_many relations, e.g. to CustomerCountry and CustomerSetting. Often, we need to join these relations to each other; e.g. to find the settings of customers in a given country. The normal way of expressing this would be something like
CustomerSetting.find :all,
:joins => {:customer => :customer_country},
:conditions => ['customer_countries.code = ?', 'us']
but the equivalent SQL ends up as
SELECT ... FROM customer_settings
INNER JOIN customers ON customer_settings.customer_id = customers.id
INNER JOIN customer_countries ON customers.id = customer_countries.customer_id
when what I really want is
SELECT ... FROM customer_settings
INNER JOIN countries ON customer_settings.customer_id = customer_countries.customer_id
I can do this by explicitly setting the :joins SQL, but is there an idiomatic way to specify this join?
Besides of finding it a bit difficult wrapping my head around the notion that you have a "country" which belongs to exactly one customer:
Why don't you just add another association in your model, so that each setting has_many customer_countries. That way you can go
CustomerSetting.find(:all, :joins => :customer_countries, :conditions => ...)
If, for example, you have a 1-1 relationship between a customer and her settings, you could also select through the customers:
class Customer
has_one :customer_setting
named_scope :by_country, lambda { |country| ... }
named_scope :with_setting, :include => :custome_setting
...
end
and then
Customer.by_country('us').with_setting.each do |cust|
setting = cust.customer_setting
...
end
In general, I find it much more elegant to use named scopes, not to speak of that scopes will become the default method for finding, and the current #find API will be deprecated with futures versions of Rails.
Also, don't worry too much about the performance of your queries. Only fix the things that you actually see perform badly. (If you really have a critical query in a high-load application, you'll probably end up with #find_by_sql. But if it doesn't matter, don't optimize it.

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

Rails - Find results from two join tables

I have have 3 Tables of data and 2 Join Tables connecting everything. I'm trying to figure out a way to query the results based on the condition that the join table data is the same.
To explain, I have User, Interest, and Event Tables. These tables are linked through an HABTM relationship (which is fine for my needs since I dont need to store any other fields) and joined through two join tables. So i also have a UsersInterests table with (user_id, interest_id) and a EventsInterests table with (event_id, interest_id).
The problem comes when trying to query all the Events where the users interests match the events interests.
I thought it would look something like this...
#events= Event.find(:all, :conditions => [#user.interests = #event.interests])
but I get the error
"undefined method `interests' for nil:NilClass", Is there something wrong with my syntax or my logic?
You're problem is that either #user or #event is undefined. Even if you define them, before executing this statement, the conditions option supplied is invalid, [#user.interests = #event.interests].
This named scope on events should do the trick
class Event < ActiveRecord::Base
...
named_scope :shares_interest_with_user, lambda {|user|
{ :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
"LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
:conditions => ["ui.user_id = ?", user], :group_by => "events.id"
}
end
#events = Event.shares_interest_with_user(#user)
Given Event <-> Interest <-> User query all the Events where the users interests match the events interests (so the following will find all such Events that this event's interest are also interests of at least one user).
First try, the simplest thing that could work:
#events = []
Interest.all.each do |i|
i.events.each do |e|
#events << e if i.users.any?
end
end
#events.uniq!
Highly inefficient, very resource hungry and cpu intensive. Generates lots of sql queries. But gets the job done.
Second try should incorporate some complicated join, but the more I think about it the more I see how vague your problem is. Be more precise.
Not sure I completely follow what you are trying to do. If you have one user and you want all events that that user also has interest in then something like:
Event.find(:all, :include => [:events_interests], :conditions => ['events_interests.interest_id in (?)', #user.interests.collect(&:id)])
should probably work.

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