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

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..

Related

ActiveRecord - find records that its association count is 0

In my Ruby on Rails app I have the following model:
class SlideGroup < ApplicationRecord
has_many :survey_group_lists, foreign_key: 'group_id'
has_many :surveys, through: :survey_group_lists
end
I want to find all orphaned slide groups. Orphaned slide group is slide group which is not connected to any survey. I've been trying following query but it does not return anything and I'm sure that I have orphaned records in my test database:
SlideGroup.joins(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
this generates following sql query:
SlideGroup Load (9.3ms) SELECT "slide_groups".* FROM "slide_groups" INNER JOIN "survey_group_lists" ON "survey_group_lists"."group_id" = "slide_groups"."id" INNER JOIN "surveys" ON "surveys"."id" = "survey_group_lists"."survey_id" GROUP BY slide_groups.id, surveys.id HAVING (count(surveys.id) = 0)
You're using joins, which is INNER JOIN, whereas what you need is an OUTER JOIN -
includes:
SlideGroup.includes(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
A bit cleaner query:
SlideGroup.includes(:surveys).where(surveys: { id: nil })
Finding orphan records has been explained by others.
I see problems with this approach:
There should not be any orphan in the first place
The presence of a survey.id does not guarantee the presence of a Survey
What about SurveyGroupList that are orphan?
So the proper solution would be to ensure that no orphans are left in the DB. By implementing the proper logic AND adding foreign keys with on delete cascade to the DB. You can also add dependent: :destroy option to your associations but this only works if you use #destroy on your models (not delete) and of course does not work if you delete directly via SQL.

How to join on the same table multiple times?

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!

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

Includes still result in second database query when using relation with limited columns

I'm trying to use includes on a query to limit the number of subsequent database calls that fire when rendering but I also want the include calls to select a subset of columns from the related tables. Specifically, I want to get a set of posts, their comments, and just the name of the user who wrote each comment.
So I added
belongs_to :user
belongs_to :user_for_display, :select => "users.id, user.name", :class_name => "User", :foreign_key => "user_id"
to my comments model.
From the console, when I do
p = Post.where(:id => 1).includes(comments: [:user_for_display])
I see that the correct queries fire:
SELECT posts.* FROM posts WHERE posts.id = 1
SELECT comments.* FROM comments comments.attachable_type = "Post" AND comments.attachable_id IN (1)
SELECT users.id, users.name FROM users WHERE users.id IN (1,2,3)
but calling
p.first.comments.first.user.name
still results in a full user load database call:
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 11805 LIMIT 1
=> "John"
Referencing just p.first.comments does not fire a second comments query. And if I include the full :user relation instead of :user_for_display, the call to get the user name doesn't fire a second users query (but i'd prefer not to be loading the full user record).
Is there anyway to use SELECT to limit fields in an includes?
You need to query with user_for_display instead of user.
p.first.comments.first.user_for_display.name

when use has_many :through in rails, can a query yield fields from all three tables?

In my app, the main objects are Accounts and Phones, with a typical has_many :through Contacts, eg:
Account:
has_many :contacts
has_many :phones, :though => contacts
Phone:
has_many :contacts
has_many :accounts, :though => :contacts
Contact:
belongs_to :account
belongs_to :phone
Contacts has fields signup_status, name
There is one Contact per unique Account/Phone pair
For an account with id = 123, which has 5 contacts, each contact having one phone, is there a query that would yield all 5 rows and include all the account fields AND contact fields AND phone fields?
You can use eager loading of associations to get all the data you need in one active record query
#account = Account.includes(:contacts, :phones).find(123)
, which will actually translate into three SQL queries:
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 123]]
SELECT "contacts".* FROM "contacts" WHERE "contacts"."account_id" IN (123)
SELECT "phones".* FROM "phones" WHERE "phones"."id" IN (<phone ids found in prev query>)
All of the records will be loaded into memory and become available through #account. To get the array of contacts and phones, just call #account.contacts and #account.phones, respectively. Note that these calls will not result in re-issued SQL queries, which is the beauty of eager loading.
ActiveRecord isn't quite smart enough to do all that with one SQL query. You can get pretty close, however, by using includes, which will avoid n+1 queries.
Account.includes(:contacts => :phones).where(:id => 123)
ActiveRecord will execute one query to load all Account records, one query to load all Contacts, and one query to load all Phones. See the link below to the documentation for the reason behind this.
if you really wanted to get everything in one SQL query (which can have drawbacks) you should look at ActiveRecord::Associations::Preloader (documentation)

Resources