complex join in scope in Rails ActiveRecord - sql

I have the following scope, which I know is not optimal:
scope :event_stream_for, lambda{ |user|
where("target_id in (?) and target_type = ?", user.events.collect(&:id), "Event")
}
This creates 3 queries. How can I optimize it?
Alternatively, how do I put the whole sql statement in lambda of the scope, like
SELECT * FROM activities WHERE target_type =='Event' AND target_id IN (SELECT DISTINCT id FROM events WHERE (host_id == user.id OR invitee_id == user.id))
Thank you

Assuming user has many events and each event belongs to a user.
scope :event_stream_for, lambda{ |user
joins(:events). # or joins("LEFT JOIN events ON events.user_id = users.id").
where(["target_type=?", "Event"])
}
this will run one query. Haven't tested my code with your table, but it should work similarly.
----------------------- based on your edited question -------------------
SELECT * FROM activities WHERE target_type =='Event' AND target_id IN (SELECT DISTINCT id FROM events WHERE (host_id == user.id OR invitee_id == user.id))
It's all about ActiveRecord Relation, http://railscasts.com/episodes/239-activerecord-relation-walkthrough?view=asciicast
Activity.
select("*,distinct events.id AS events_id").
joins("events ON (events.host_id = #{user.id} OR events.invitee_id = #{user.id}").
where(:target_type => 'Event')
Try this on console, and if it works you can just simply change it to scope.
Since i m not sure what you are trying to do, you may need some adjustment.

How about this?
scope :event_stream_for, lambda{ |user|
where("target_id in (SELECT DISTINCT id FROM events WHERE (host_id == ? OR invitee_id == ?) and target_type = ?", user.id, user.id, "Event")
}
It's just rearranging what you already had, but it should get you down to one query, since it doesn't use the associations in code.

Related

Is there a way to combine where and where.not into one condition in Rails?

I have an Event model, that has user_id inside it. I want to select all objects of this model, with specified user_id but not including specific events. So I can do it with a query like that:
Event.where(user_id: user.id).where.not(id: id)
But can I combine these 2 where functions into one?
I know that if I need to find, for example, events with specified ids and user_ids, I can do it this way:
Event.where(user_id: user_id).where(id: id)
and I can compact it using one where call instead of two:
Event.where(user_id: user_id, id: id)
but can I do the same thing if I am using where and where.not?
You can gather
Event.where(user_id: 1) + Event.where.not(id: 2)
or deny a parameter
Event.where(user_id: 1).where.not(id: 2)
You can write as per below to add where and where.not :
Event.where(
"user_id = ? AND id != ?",
user.id,
id
)
so if user_id = 1 and id = 2
than this will return records with user_id 1 and without id 2 :)
try this,you can create two scopes and calling then in chain
scope :with_user, ->(user) {user_id: user.id}
scope :excluded_event, ->(event_ids) { where.not(id: event_ids) }
Event.with_user(user).excluded_event(event_ids)

SQL LEFT JOIN value NOT in either join column

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.

rails 4 complex SQL scope

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.

Nested sql queries in rails when :has_and_belongst_to_many

In my application I the next task that has not already been done by a user. I have Three models, A Book that has many Tasks and then I have a User that has has and belongs to many tasks. The table tasks_users table contains all completed tasks so I need to write a complex query to find the next task to perform.
I have came up with two solutions in pure SQL that works, but I cant translate them to rails, thats what I need help with
SELECT * FROM `tasks`
WHERE `tasks`.`book_id` = #book_id
AND `tasks`.`id` NOT IN (
SELECT `tasks_users`.`task_id`
FROM `tasks_users`
WHERE `tasks_users`.`user_id` = #user_id)
ORDER BY `task`.`date` ASC
LIMIT 1;
and equally without nested select
SELECT *
FROM tasks
LEFT JOIN tasks_users
ON tasks_users.tasks_id = task.id
AND tasks_users.user_id = #user_id
WHERE tasks_users.task_id IS NULL
AND tasks.book_id = #book_id
LIMIT 1;
This is what I Have done in rails with the MetaWhere plugin
book.tasks.joins(:users.outer).where(:users => {:id => nil})
but I cant figure out how to get the current user there too,
Thanks for any help!
I think this will duplicate the second form with the LEFT JOIN:
class Task < ActiveRecord::Base
scope :next_task, lambda { |book,user| book.tasks.\
joins("LEFT JOIN task_users ON task_users.task_id=tasks.id AND task_users.user_id=#{user.id}").\
where(:tasks=>{:task_users=>{:task_id=>nil}}).\
order("date DESC").limit(1) }
end
Note that instead of tasks_users this uses the table name task_user, which is more typical for a join model. Also, it needs to be called with:
Task.next_task(#book_id,#user_id)
book.tasks.where("tasks.id not in (select task_id from tasks_users where user_id=?)", #user_id).first
That would give you the first task that doesn't already have an entry in tasks_users for the current user.

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.