How to join on the same table multiple times? - ruby-on-rails

I'm querying for the mutual friends of a given two users. The query below should do the trick for the most part and the friendship table should be self-evident, containing a user_id and friend_id.
SELECT `users`.* FROM `users`
INNER JOIN `friendships` `a` ON `users`.`id` = `a`.`friend_id`
INNER JOIN `friendships` `b` ON `users`.`id` = `b`.`friend_id`
WHERE `a`.`user_id` = 1 AND `b`.`user_id` = 2
What's got me confused is how to write this semantic ActiveRecord. With ActiveRecord you can join on an association, but only once. So how do you go about writing this as plainly as possible in ActiveRecord?

I do it with string arguments to joins:
User.
joins("INNER JOIN friendships a ON users.id = a.friend_id").
joins("INNER JOIN friendships b ON users.id = b.friend_id").
where("a.user_id" => 1, "b.user_id" => 2)
I'm not aware of a higher-level way to do such a join with Active Record.

Firstly, you should have proper Model Relationship between User & Friendship.
user model:
has_many :friendships
friendship model:
belongs_to :user
with that:
you can get activerecords in your iteration like:
users = User.all
users.each do |user|
user.friendships
.....
end
Or, by specific user, like:
user = User.first
user.friendships #returns association
or
User.first.friendships
for 2 specific users (given), you can do like:
User.where(id: [1,2]) #array of user id, then get the friendship record as above.
Hope this helps!

Related

How to do this PG query in Rails?

I have the following simple relations:
class Company
has_many :users
end
class User
belongs_to :company
has_and_belongs_to_many :roles
end
class Role
has_and_belongs_to_many :users
end
The only column that matters is :name on Role.
I'm trying to make an efficient PostgreSQL query which will show a comma separated list of all role_names for each user.
So far I have got it this far, which works great if there's only single role assigned. If I add another role, I get duplicate users. Rather than trying to parse this after, I'm trying to just get it to return a comma separated list in a role_names field by using the string_agg() function.
This is my query so far and I'm kind of failing at taking it this final step.
User.where(company_id: id)
.joins(:roles)
.select('distinct users.*, roles.name as role_name')
EDIT
I can get it working via raw SQL (gross) but rails doesn't know how to understand it when I put it in ActiveRecord format
ActiveRecord::Base.connection.execute('SELECT users.*, string_agg("roles"."name", \',\') as roles FROM "users" INNER JOIN "roles_users" ON "roles_users"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "roles_users"."role_id" WHERE "users"."company_id" = 1 GROUP BY users.id')
User.where(company_id: id)
.joins(:roles)
.select('users.*, string_agg("roles"."name" \',\')')
.group('users.id')
Looks to me that you want to do:
User.roles.map(&:name).join(',')
(In my opionion SQL is a better choice when working with databases but when you are on rails you should probably do as much as possible with Active Record. Be aware of performance issues!)

Group and count active record Rails

I have two tables, users and votations, in votations have two fields win_id and user_id.
I need create query in active record with win_id (votation) and id (users).
My sql query like this:
Select users.name, Count(votations.win_id) from votations Inner Join users ON vontations.win_id = users.id Group By users.name;
Assuming you have an association in Violation like this:
belongs_to :user, foreign_key: "win_id"
Then this should do it:
Violation.joins(:user).group("users.name").count
It will give you a hash with the user name as the key and the count as the value.
If you don't have that association and don't want it, then change the joins() to this:
joins("JOIN users ON violations.win_id = users.id")
The clearest way may be to just use find_by_sql (docs here):
User.find_by_sql("Select users.name, Count(votations.win_id) from votations Inner Join users ON vontations.win_id = users.id Group By users.name")

Implement connections (FB-like) between users on a RoR app

I'm trying to implement a Self-Referential Association in order to achieve a connections list for a given user (like FB or linkedIn does).
All the tutorials around implement the "following/follower" model that is a bit diffrent from this one, so... let's get creative:
So besides the user table, I have a user_connections table with the fields:
requester_id
requested_id
(...)
And "modeled" it the following way:
User.rb:
# Connections
has_many :user_connections, foreign_key: 'requester_id' # people that i invited to connect to me
has_many :user_inverse_connections, foreign_key: 'requested_id', class_name: 'UserConnection' # people that invited me to connect with them
has_many :i_invited, source: :requester, through: :user_connections
has_many :invited_me, source: :requested, through: :user_inverse_connections
def connections
i_invited.merge(invited_me)
end
I tried to test this:
2.0.0-p247 :003 > u.connections
User Load (84.3ms) SELECT `users`.* FROM `users` INNER JOIN `user_connections` ON `users`.`id` = `user_connections`.`requester_id` INNER JOIN `user_connections` ON `users`.`id` = `user_connections`.`requested_id` WHERE `user_connections`.`requester_id` = 1 AND `user_connections`.`requested_id` = 1
But as may noticed, it didn't work out:
Mysql2::Error: Not unique table/alias: 'user_connections': SELECT
users.* FROM users INNER JOIN user_connections ON users.id =
user_connections.requester_id INNER JOIN user_connections ON
users.id = user_connections.requested_id WHERE
user_connections.requester_id = 1 AND
user_connections.requested_id = 1
Am I doing this really the right way?
Furtherly, any tips on how I can achieve "connection" search for 2nd level and others?
You can make a table with attributes follower_id, follows_id
And just record who every user follows in that table. This is a Many-Many relationship
So John can be following Jane even though Jane does not follow John.
def connections
i_invited + invited_me
end
This solved the problem...
But with a pay-off: (haven't tested yet, but I think) it's an array instead of a relation..

How to merge (link) 2 Relations on different tables in Rails 4

Given 2 ActiveRecord relations that generate following SQL:
relation a = SELECT comments.* FROM comments INNER JOIN attachments ON attachments.comment_id = comments.id WHERE attachment.name ILIKE '%foo%
relation b = SELECT attachments.* FROM attachments INNER JOIN users ON attachments.user_id = users.id WHERE users.other_conditions
This worked in Rails/ActiveRecord 3:
puts a.merge(b).to_sql # Rails 3
> "SELECT comments.* FROM comments INNER JOIN attachments ON attachments.comment_id = comments.id INNER JOIN users ON attachments.user_id = users.id WHERE attachment.name ILIKE '%foo% AND users.other_conditions"
I think it worked because the merge was ignoring any non-existing associations on the queries.
But Rails 4 is much more pedantic and fails with:
puts a.merge(b).to_sql # Rails 4
> ActiveRecord::ConfigurationError: Association named 'user' was not found on Comment; perhaps you misspelled it?
So the question is how can I literally merge the 2 relations without Rails being worried about the correctness (my specs take responsibility for that)?
Can you describe your models and their relations a little more?
For me it worked like this:
class User
has_many :facebook_friends
end
class FacebookFriend
belongs_to :user
end
a = User.where("users.first_name LIKE '%Sandy%'")
b = FacebookFriend.where("facebook_friends.last_name LIKE '%John%'")
a.merge(b)
=> User Load (0.5ms) SELECT users.* FROM users WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
=> Mysql2::Error: Unknown column 'facebook_friends.last_name' in 'where clause': SELECT users.* FROM users WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
a.joins(:facebook_friends).merge(b)
=> User Load (0.6ms) SELECT users.* FROM users INNER JOIN facebook_friends ON facebook_friends.user_uid = users.uid WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
=> []
The amazing scuttle.io transforms your sql as follows:
Comment.select(Comment.arel_table[Arel.star]).where(
Attachment.arel_table[:name].and(User.arel_table[:other_conditions])
).joins(
Comment.arel_table.join(Attachment.arel_table).on(
Attachment.arel_table[:comment_id].eq(Comment.arel_table[:id])
).join_sources
).joins(
Comment.arel_table.join(User.arel_table).on(
Attachment.arel_table[:user_id].eq(User.arel_table[:id])
).join_sources
)

Specifying conditions on eager loaded associations returns ActiveRecord::RecordNotFound

The problem is that when a Restaurant does not have any MenuItems that match the condition, ActiveRecord says it can't find the Restaurant. Here's the relevant code:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
def self.with_meals_of_the_week
includes({menu_items: :meal}).where(:'menu_items.date' => Time.now.beginning_of_week..Time.now.end_of_week)
end
end
And the sql code generated:
Restaurant Load (0.0ms)←[0m ←[1mSELECT DISTINCT "restaurants".id FROM "restaurants"
LEFT OUTER JOIN "menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN "meals" ON "meals"."id" = "menu_items"."meal_id" WHERE
"restaurants"."id" = ? AND ("menu_items"."date" BETWEEN '2012-10-14 23:00:00.000000'
AND '2012-10-21 22:59:59.999999') LIMIT 1←[0m [["id", "1"]]
However, according to this part of the Rails Guides, this shouldn't be happening:
Post.includes(:comments).where("comments.visible", true)
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded.
The SQL generated is a correct translation of your query. But look at it,
just at the SQL level (i shortened it a bit):
SELECT *
FROM
"restaurants"
LEFT OUTER JOIN
"menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN
"meals" ON "meals"."id" = "menu_items"."meal_id"
WHERE
"restaurants"."id" = ?
AND
("menu_items"."date" BETWEEN '2012-10-14' AND '2012-10-21')
the left outer joins do the work you expect them to do: restaurants
are combined with menu_items and meals; if there is no menu_item to
go with a restaurant, the restaurant is still kept in the result, with
all the missing pieces (menu_items.id, menu_items.date, ...) filled in with NULL
now look aht the second part of the where: the BETWEEN operator demands,
that menu_items.date is not null! and this
is where you filter out all the restaurants without meals.
so we need to change the query in a way that makes having null-dates ok.
going back to ruby, you can write:
def self.with_meals_of_the_week
includes({menu_items: :meal})
.where('menu_items.date is NULL or menu_items.date between ? and ?',
Time.now.beginning_of_week,
Time.now.end_of_week
)
end
The resulting SQL is now
.... WHERE (menu_items.date is NULL or menu_items.date between '2012-10-21' and '2012-10-28')
and the restaurants without meals stay in.
As it is said in Rails Guide, all Posts in your query will be returned only if you will not use "where" clause with "includes", cause using "where" clause generates OUTER JOIN request to DB with WHERE by right outer table so DB will return nothing.
Such implementation is very helpful when you need some objects (all, or some of them - using where by base model) and if there are related models just get all of them, but if not - ok just get list of base models.
On other hand if you trying to use conditions on including tables then in most cases you want to select objects only with this conditions it means you want to select Restaurants only which has meals_items.
So in your case, if you still want to use only 2 queries (and not N+1) I would probably do something like this:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
cattr_accessor :meals_of_the_week
def self.with_meals_of_the_week
restaurants = Restaurant.all
meals_of_the_week = {}
MenuItems.includes(:meal).where(date: Time.now.beginning_of_week..Time.now.end_of_week, restaurant_id => restaurants).each do |menu_item|
meals_of_the_week[menu_item.restaurant_id] = menu_item
end
restaurants.each { |r| r.meals_of_the_week = meals_of_the_week[r.id] }
restaurants
end
end
Update: Rails 4 will raise Deprecation warning when you simply try to do conditions on models
Sorry for possible typo.
I think there is some misunderstanding of this
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any
posts, all the posts would still be loaded. By using joins (an INNER
JOIN), the join conditions must match, otherwise no records will be
returned.
[from guides]
I think this statements doesn't refer to the example Post.includes(:comments).where("comments.visible", true)
but refer to one without where statement Post.includes(:comments)
So all work right! This is the way LEFT OUTER JOIN work.
So... you wrote: "If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded." Ok! But this is true ONLY when there is NO where clause! You missed the context of the phrase.

Resources