I have three models: Event, Comment and Photo. An Event has both has_many :comments and has_many :photos.
My goal is to find all Events which have received new comments and/or photos in the last 24 hours.
Assume that two events exist, one with a recent comment and another with a recent photo.
If I query them separately with a single join, everything works as expected:
Event.joins(:comments).where("comments.created_at >= :today", :today => Time.now.beginning_of_day)
returns: [#<Event id: 1>]
Event.joins(:photos).where("photos.created_at >= :today", :today => Time.now.beginning_of_day)
returns: [#<Event id: 2>]
Why is it when I combine both joins:
Event.joins(:comments, :photos)
.where("comments.created_at >= :today OR photos.created_at >= :today", :today => Time.now.beginning_of_day)
that I receive [#<Event id: 2>, #<Event id: 2>] — twice #2, but not #1?
The SQL generated by ARel is
SELECT "events".*
FROM "events"
INNER JOIN "comments"
ON "comments"."event_id" = "events"."id"
INNER JOIN "photos"
ON "photos"."event_id" = "events"."id"
WHERE ( comments.created_at >= '2013-03-25 23:00:00.000000'
OR photos.created_at >= '2013-03-25 23:00:00.000000' )
I believe it is because of the INNER JOIN. This query would bring ONLY the events that has both photos and comments associated with it.
You can try using LEFT JOIN instead of INNER on your RDBMS and see that both results are fetched.
It's an SQL problem : you have to specify LEFT JOIN instead of INNER JOIN. include will do it for you :
Event.include(:comments, :photos).where(...).uniq
Related
I have two models in a Rails app - Tournament and Player associated through a join table:
class Tournament < ApplicationRecord
has_many :tournament_players
has_many :players, through: :tournament_players
end
class Player < ApplicationRecord
has_many :tournament_players
has_many :tournaments, through: :tournament_players
scope :selected, -> (tournament) { includes(:tournaments).where(tournaments: {id: tournament.id}) }
end
I have lots of Tournaments, and each one can have lots of Players. Players can play in lots of Tournaments. The scope
scope :selected, -> (tournament) { includes(:tournaments).where(tournaments: {id: tournament.id}) }
successfuly finds all the players already added to a tournament, given that tournament as an argument.
What I'd like is a scope that does the opposite - returns all the players not yet added to a given tournament. I've tried
scope :not_selected, -> (tournament) { includes(:tournaments).where.not(tournaments: {id: tournament.id}) }
but that returns many of the same players, I think because the players exist as part of other tournaments. The SQL for that looks something like:
SELECT "players".*, "tournaments”.* FROM "players" LEFT OUTER JOIN
"tournament_players" ON "tournament_players"."player_id" =
"players"."id" LEFT OUTER JOIN "tournaments" ON "tournaments"."id" =
"tournament_players"."tournament_id" WHERE ("tournaments"."id" != $1)
ORDER BY "players"."name" ASC [["id", 22]]
I've also tried the suggestions on this question - using
scope :not_selected, -> (tournament) { includes(:tournaments).where(tournaments: {id: nil}) }
but that doesn't seem to work - it just returns an empty array, again I think because the Players exist in the join table as part of a separate Tournament. The SQL for that looks something like:
SELECT "players”.*, "tournaments”.* FROM "players" LEFT OUTER JOIN
"tournament_players" ON "tournament_players"."player_id" =
"players"."id" LEFT OUTER JOIN "tournaments" ON "tournaments"."id" =
"tournament_players"."tournament_id" WHERE "tournaments"."id" IS NULL
ORDER BY "players"."name" ASC
What you need to do is:
Make a left join with the reference table, with an additional condition on the tournament ID matching the one that you want to find the not-selected players for
Apply a WHERE clause indicating that there was no JOIN made.
This code should do it:
# player.rb
scope :not_selected, -> (tournament) do
joins("LEFT JOIN tournament_players tp ON players.id = tp.player_id AND tp.tournament_id = #{tournament.id}").where(tp: {tournament_id: nil})
end
If only Rails had a nicer way to write LEFT JOIN queries with additional conditions...
A few notes:
Don't join the actual relation (i.e. Tournament), it dramatically decreases performance of your query, and it's unnecessary, because all your condition prerequisites are inside the reference table. Besides, all the rows you're interested in return NULL data from the tournaments table.
Don't use eager_load. Besides to my best knowledge its not supporting custom conditions, it would create models for all related objects, which you don't need.
ok try this:
includes(:tournaments).distinct.where.not(tournaments: {id: tournament.id})
I suspect this is a rather common scenario and may show my ineptitude as a DB developer, but here goes anyway ...
I have two tables: Profiles and HiddenProfiles and the HiddenProfiles table has two relevant foreign keys: profile_id and hidden_profile_id that store ids from the Profiles table.
As you can imagine, a user can hide another user (wherein his profile ID would be the profile_id in the HiddenProfiles table) or he can be hidden by another user (wherein his profile ID would be put in the hidden_profile_id column). Again, a pretty common scenario.
Desired Outcome:
I want to do a join (or to be honest, whatever would be the most efficient query) on the Profiles and HiddenProfiles table to find all the profiles that a given profile is both not hiding AND not hidden from.
In my head I thought it would be pretty straightforward, but the iterations I came up with kept seeming to miss one half of the problem. Finally, I ended up with something that looks like this:
SELECT "profiles".* FROM "profiles"
LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = 1)
LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = 1)
WHERE (hp1.hidden_profile_id is null) AND (hp2.profile_id is null)
Don't get me wrong, this "works" but in my heart of hearts I feel like there should be a better way. If in fact there is not, I'm more than happy to accept that answer from someone with more wisdom than myself on the matter. :)
And for what it's worth these are two RoR models sitting on a Postgres DB, so solutions tailored to those constraints are appreciated.
Models are as such:
class Profile < ActiveRecord::Base
...
has_many :hidden_profiles, dependent: :delete_all
scope :not_hidden_to_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = #{profile.id})").where("hp1.hidden_profile_id is null") }
scope :not_hidden_by_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = #{profile.id})").where("hp2.profile_id is null") }
scope :not_hidden, -> (profile) { self.not_hidden_to_me(profile).not_hidden_by_me(profile) }
...
end
class HiddenProfile < ActiveRecord::Base
belongs_to :profile
belongs_to :hidden_profile, class_name: "Profile"
end
So to get the profiles I want I'm doing the following:
Profile.not_hidden(given_profile)
And again, maybe this is fine, but if there's a better way I'll happily take it.
If you want to get this list just for a single profile, I would implement an instance method to perform effectively the same query in ActiveRecord. The only modification I made is to perform a single join onto a union of subqueries and to apply the conditions on the subqueries. This should reduce the columns that need to be loaded into memory, and hopefully be faster (you'd need to benchmark against your data to be sure):
class Profile < ActiveRecord::Base
def visible_profiles
Profile.joins("LEFT OUTER JOIN (
SELECT profile_id p_id FROM hidden_profiles WHERE hidden_profile_id = #{id}
UNION ALL
SELECT hidden_profile_id p_id FROM hidden_profiles WHERE profile_id = #{id}
) hp ON hp.p_id = profiles.id").where("hp.p_id IS NULL")
end
end
Since this method returns an ActiveRecord scope, you can chain additional conditions if desired:
Profile.find(1).visible_profiles.where("created_at > ?", Time.new(2015,1,1)).order(:name)
Personally I've never liked the join = null approach. I find it counter intuitive. You're asking for a join, and then limiting the results to records that don't match.
I'd approach it more as
SELECT id FROM profiles p
WHERE
NOT EXISTS
(SELECT * FROM hidden_profiles hp1
WHERE hp1.hidden_profile_id = 1 and hp1.profile_id = p.profile_id)
AND
NOT EXISTS (SELECT * FROM hidden_profiles hp2
WHERE hp2.hidden_profile_id = p.profile_id and hp2.profile_id = 1)
But you're going to need to run it some EXPLAINs with realistic volumes to be sure of which works best.
I have a model Users which has_many EventLogs.
I would like create a scope which will order Users by those with the most occurrences of EventLogs they have.
scope :highest_completed_events, .....
How can I count the number of EventLogs with a status of 2, and then order the users with the highest occurrence of that type of event.
User.joins(:event_logs).where("event_logs.status_id = 2")#... COUNT, then ORDER BY
Hope that makes sense.
Here's a query you can execute to get your users ordered by the number of events they have:
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC')
You should use a LEFT JOIN since you'll want to include users who don't have any events.
If you were to write it as a scope:
scope(:highest_completed_events, {
select: 'users.*, COUNT(event_logs.id) as event_logs_count',
joins: 'LEFT JOIN event_logs ON event_logs.user_id = users.id',
group: 'users.id',
order: 'event_logs_count DESC'
})
#users = User.highest_completed_events
In order to filter the events by a particular status, simply use a where().
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
where('event_logs.status = ?', STATUS_COMPLETE).
group('users.id').
order('event_logs_count DESC')
As an aside, sometimes you'll run into issues with ActiveRecord stripping out your custom select() statement when doing something like #users.count. What I normally do is nest this kind of thing in a custom from() statement.
_from = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC').to_sql
#users = User.from("(#{_from}) as users")
#users.count # will work
Try:
User.all.sort_by{|u| u.event_logs.select{|l| l.status_id = 2}.count}.reverse
Or is it 'eventlogs'? Schouldn't your line be has_many :event_logs ?
BTW, my solution is not very efficient but DB-agnostic.
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
I have a model offers and another historical_offers, one offer has_many historical_offers.
Now I would like to eager load the historical_offers of one given day for a set of offers, if it exists. For this, I think I need to pass the day to the ON clause, not the WHERE clause, so that I get all offers, also when there is no historical_offer for the given day.
With
Offer.where(several_complex_conditions).includes(:historical_offers).where("historical_offers.day = ?", Date.today)
I would get
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id
WHERE day = '2012-11-09' AND ...
But I want to have the condition in the ON clause, not in the WHERE clause:
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id AND day = '2012-11-09'
WHERE ...
I guess I could alter the has_many definition with a lambda condition for a specific date, but how would I pass in a date then?
Alternatively I could write the joins mysqlf like this:
Offer.where(several_complex_conditions)
.joins(["historical_offers ON offers.id = historical_offers.offer_id AND day = ?", Date.today])
But how can I hook this up so that eager loading is done?
After a few hours headscratching and trying all sorts of ways to accomplish eager loading of a constrained set of associated records I came across #dbenhur's answer in this thread which works fine for me - however the condition isn't something I'm passing in (it's a date relative to Date.today). Basically it is creating an association with the conditions I wanted to put into the LEFT JOIN ON clause into the has_many condition.
has_many :prices, order: "rate_date"
has_many :future_valid_prices,
class_name: 'Price',
conditions: ['rate_date > ? and rate is not null', Date.today-7.days]
And then in my controller:
#property = current_agent.properties.includes(:future_valid_prices).find_by_id(params[:id])