Rails: join with multiple conditions - ruby-on-rails-3

I have a simple model like
class Interest < ActiveRecord::Base
has_and_belongs_to_many :user_profiles
end
class UserProfile < ActiveRecord::Base
has_and_belongs_to_many :interests
end
When I want to query all the users with a specific interests, that's fairly simple doing
UserProfile.joins(:interests).where('interests.id = ?', an_interest)
But how can I look for users who have multiple interests? Of course if I do
UserProfile.joins(:interests).where('interests.id = ?', an_interest).where('interests.id = ?', another_interest)
I get always an empty result, since after the join, no row can have simultaneously interest.id = an_interest and interest.id = another_interest.
Is there a way in ActiveRecord to express "I want the list of users who have 2 (specified) interests associated?
update (solution) that's the first working version I came up, kudos to Omar Qureshi
specified_interests.each_with_index do |i, idx|
main_join_clause = "interests_#{idx}.user_profile_id = user_profiles.id"
join_clause = sanitize_sql_array ["inner join interests_user_profiles interests_#{idx} on
(#{main_join_clause} and interests_#{idx}.interest_id = ?)", i]
relation = relation.joins(join_clause)
end

in (?) is no good - it's an OR like expression
what you will need to do is have multiple joins written out longhanded
profiles = UserProfile
interest_ids.each_with_index do |i, idx|
main_join_clause = "interests_#{idx}.user_profile_id = user_profiles.id"
join_clause = sanitize_sql_array ["inner join interests interests_#{idx} on
(#{main_join_clause} and interests_#{idx}.id = ?)", i]
profiles = profiles.join(join_clause)
end
profiles
You may need to change the main_join_clause to suit your needs.

This will get users that have at least one of the specified interests.
UserProfile.joins(:interests).where(:id => [an_interest_id, another_interest_id])
To get users that have both of the specified interests I'd probably do something like this:
def self.all_with_interests(interest_1, interest_2)
users_1 = UserProfile.where("interests.id = ?", interest_1.id)
users_2 = UserProfile.where("interests.id = ?", interest_2.id)
users_1 & users_2
end
Not amazingly efficient, but it should do what you need?

Try IN (?) and an array:
UserProfile.joins(:interests).where('interests.id IN (?)', [1,2,3,4,5])

Related

Rails: loading association at once vs multiple tiny queries

I've got a situation. I am rendering a paginated collection of resources(let's say Posts). I need to show whether the post is liked by the current user. So basically we have three tables: posts, users and likes(a join table o indicate if users like some posts).
I came up with three solutions:
class Post
# 1
def liked_by?(user)
likes.exists?(user: user) # usage: Post.limit(50).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 2
def liked_by?(user)
likes.any? { |like| like.user_id == user.id } # usage: Post.limit(50).includes(:likes).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 3
def liked_by?(user)
user.likes.exists?(post: self) # usage: Post.limit(50).includes(:likes).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 4
def liked_by?(user)
user.likes.any? { |like| like.user_id == user.id } # usage: Post.limit(50).map { |post| [p.id, post.liked_by?(current_user)] }
end
end
I can imagine the advantages and disadvantages of each of them but would be happy to hear any thoughts from you guys.
Usually, we render 50 posts at a time and the current user has about a hundred likes, but some users have thousands of likes so it's hard to say that there is a general solution for this. Anyway, I'm open to hear your proposals. Probably there's way # 5...(like denormalizing tables, materialized views and etc).
None of the above as they will all create a N+1 query issue. Also interrogation methods (methods that end with ?) should by convention return a boolean in Ruby.
You can use EXISTS with a subquery:
SELECT "posts".* FROM "posts"
WHERE EXISTS (SELECT "likes".* FROM "likes" WHERE "likes"."user_id" = $1 AND "likes"."post_id" = "posts"."id")
class User < ApplicationRecord
has_many :likes
end
class Post < ApplicationRecord
has_many :likes
# returns posts liked by user
def self.liked_by_user(user)
where(
user.likes
.where(Like.arel_table[:post_id].eq(arel_table[:id]))
.arel.exists
)
end
end
If you want all the posts regardless if the have been liked or not and a boolean indicating if they have been liked by the user can put that same EXISTS into the SELECT clause:
class Post < ApplicationRecord
# ...
# selects posts and a liked_by_user boolean
def self.with_likes_by_user(user)
columns = self.respond_to?(:arel) ? arel.projections : arel_table[Arel.star]
select(
columns,
user.likes
.where(Like.arel_table[:post_id].eq(arel_table[:id]))
.arel.exists.as('liked_by_user')
)
end
end
SELECT
"posts".*,
EXISTS (
SELECT "likes".*
FROM "likes" WHERE "likes"."user_id" = $1
AND "likes"."post_id" = "posts"."id"
) AS liked_by_user
FROM "posts"

undefined method `order' for []:Array

I have the following code where I load the activities for the user based on whether or not they are an admin. Users can only see their own activity feed, whereas admins can see the entire stream. This is a precursor to sharing activity feeds with friends, etc.
def index
if current_user?
#incidents = Incident.find_all_by_user_id(current_user.id).order("created_at desc")
else
#incidents = Incident.all.order("created_at desc")
end
end
I am getting the above referenced error(undefined method "order" for []:Array). It seems to be the .order reference, but I have checked the rails Guides and it seems to be correct syntax.
Any ideas?
Try changing the find_by... to where, so:
def index
if current_user?
#incidents = Incident.where(user_id: current_user.id).order("created_at desc")
else
#incidents = Incident.all.order("created_at desc")
end
end
should work :-)
The #index action method can be simplified and optimized (by replacement find_all_by with where) to:
def index
clause = current_user? && Incident.where(user_id: current_user.id) || Incident
#incidents = clause.order("created_at desc")
end

how can I hide resources?

I want to create a page where it shows the resource created by other users but hide the resources created by current_user. is there a method or certain way in which I can do so?
class ExamplesController < ApplicationController
def index
#examples = Example.all.order("created_at DESC")
#creator_examples = Example.where(creator: current_user).order("created_at DESC") <---hide this!!!
end
You can simply manipulate your where clause into something like this:
def index
#examples = Example.all.order("created_at DESC")
#creator_examples = #examples.where.not(id: current_user.id)
end
This is for rails 4, if you're using rails 3
#creator_examples = Example.where("id != ?", current_user.id)
Note -> Example.all in rails 3 returns an array so you can't chain it with where

Get associated ActiveRecord::Relation from complex query

I have the following AR models:
class Checkin < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :checkins
end
Let's say I have a complex query on checkins and users, for example, Checkin.nearby.today and User.friends_of(john). Is there a straightforward way I can derive an ActiveRecord::Relation of Users? The end result would be friends of John who have checked in nearby today.
I would like the end result to be an instance of ActiveRecord::Relation.
Thanks!
This should do it:
users = User.friends_of(john)
users = users.joins(:checkins).where(checkins: { checkin_date: Date.today })
users = users.where( # your logic to determine the nearby )
As you can see, there is the logic about the nearby scope missing.
In a custom method:
def self.friends_checked_nearby_at_date(friend, nearby = true, date = Date.today)
users = User.friends_of(friend)
users = users.joins(:checkins).where(checkins: { checkin_date: date })
users = users.where( # your logic for the nearby scope ) if nearby.present?
return users
end
# usage:
User.friends_checked_nearby_at_date( User.first )
# or
User.friends_checked_nearby_at_date( User.first, true, Date.today-1.week )
# or
User.friends_checked_nearby_at_date( User.first, false )
# etc.

pulling one field of a record into an array in a Rails 3 app

Rails 3 noob here. Currently the code in my controller below is getting the whole record in my database. I am trying to populate the array with one integer, not the whole record. The integer is contained in a table "answers" and the field name is "score". How can i modify this line in my controller to get just the one field?
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id )
end
UPDATE FOR CLARIFICATION: The :score can be any integer from 0 to 5. I would like to populate #ans[s.position] with the integer.
Thanks.
You're very close
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? and user_id = ?",s.q_id,current_user.id).select(:score).first.try(:score)
end
You need to select "score" from Answer, then you need to retrieve it from the object.
Since where could potentially return many Answers, use first to pull off the first Answer, and then score to project out the score field.
answers = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id )
score = answers.first.score
All together it would be:
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id ).first.score
end
As an optimization to only retrieve score from the database, instead of answers.*, you could use select(:score).
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id ).select(:score).first.score
end
See Active Record Query Interface for more about using select.
Just include them.
#questions = Question.includes(:answers).select(:position)
Or use this
#questions = Question.select(:position)
#questions.each{ |s| #ans[s.position] = s.answers.where(:user_id => current_user.id) }