Accessing Join Attributes in Rails 4 - sql

I am trying to get all of a user's friends' posts and user info in one query.
I have tried to use includes(:users) but that doesn't seem to work and throws an error. I figured a manual join would be the best solution:
#friends = #user.friends
.joins('inner join users on users.id = friends.user_id left outer join posts on users.id = posts.user_id')
.select('posts.*, users.*, friends.*')
How do you access attributes from users and posts? doing a #friends.inspect only shows attributes from the friends table, and I can't do #friends.posts or #friends.users.
My model for friends and users looks like this:
class Friend < ActiveRecord::Base
belongs_to :users
end
class User < ActiveRecord::Base
has_many :friends
has_many :users, through: :friends
end

According to the SQL in the join clause, your posts are related to users, not to friends. Something like this will work:
friends.map{|f| f.user.posts}.flatten
if Friend had belongs_to :user (singular "user") which would be conventional Rails, and is reflected in the SQL you used, so has the overriding factor of being correct.

Related

Active Records Query

I will go stright to the point:
I have these two models:
User
Followed
The Followed model has
user_id (the user that follows another user) and
followed_id, the user that is followed by the user with user_id.
the User model looks like this:
has_many :followeds, foreign_key: :followed_id, dependent: :destroy
the Followed model looks like this
belongs_to :user
What I want to achieve is this: I want to retrieve the followers for the current_user (the logged in user).
I have some idea on what I should do, which is something like this:
1 - join users table with followeds table
2 - select users where users.id = followeds.user_id (the user who follows the current_user)
3 - and (condition) followeds.followed_id = current_user.id (the user who is followed is the current_user, the one logged in)
I don' t know if it can help, but the following query is the one that I succesfully used (with the kind help of stack overflow users) to retrive the users that a user follows:
#users = User.joins(:followeds).where(followeds: {user_id: current_user.id})
Based on that I believe that the query should look something like
#users = User.joins(:followeds).where(followeds: {followed_id: current_user.id})
and then some query to select users.id = followeds.user_id
If I clear understand your associations
User.where(id: Followed.select(:user_id).where(followed_id: current_user.id))
It will generate something like this SQL
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM followeds
WHERE followed_id = <current_user_id>
);
You can also add association to User model with :source and :through options
has_many :followers, through: :followeds, source: :user
And than just
current_user.followers
This will generate query like this
SELECT * FROM users
INNER JOIN followeds ON users.id = followeds.user_id
WHERE followeds.followed_id = <current_user_id>;

Querying an indirect relationship

I have an app that has Users, Libraries and Books.
Users have many Libraries. Libraries have many Users and Books. But Users and Books do not have a direct relationship. Users can only borrow Books from Libraries.
I'm stuck trying to get the collection of books that belongs to the libraries that a user is a member of. In other words, I want to get all of the books in a library that a given user belongs to.
To be clear, I don't want to go through the borrowing association that currently exists between users and books, because I want all the books in that library, not just the ones currently borrowed by the user.
I've tried to come up with this query for hours using SQL and Squeel, without luck... thanks in advance for your help.
Table_User
user_id user_name library_id membership_id
Table_Library
library_id user_id book_id membership_id
Table_Books
books_id book_name library_id
Table_Membership
membership_id user_id library_id
select book_name from Table_Books
inner join Table_Library on Table_Books.library_id=Table_Library.library_id
inner join Table_User on Table_User.library_id=Table_Library.library_id
where Table_User.library_id = Table_Membership.library_id
and Table_User.user_id =Table_Membership.user_id
You can do something like this...
Book.where(:library_id => user.library_ids)
Or you can do this. Given this model setup...
class Book < ActiveRecord::Base
belongs_to :library
class Library < ActiveRecord::Base
has_many :books
has_many :memberships
has_many :users, :through => :memberships
class User < ActiveRecord::Base
has_many :memberships
has_many :libraries, :through => :memberships
has_many :books, :through => :libraries
With this, you can ask for #user.books, which in Rails will generate SQL like:
# 1 is #user.id
SELECT "books".* FROM "books" INNER JOIN "libraries" ON "books"."library_id" = "libraries"."id" INNER JOIN "memberships" ON "libraries"."id" = "memberships"."library_id" WHERE "memberships"."user_id" = 1

Sort records based on number of rows in another table - Rails

So I have a users table and in my relationship, I have defined that a user has many submissions and submissions belong to a user. I want to sort the users table based on how many submissions they have.
submission model
class Submission < ActiveRecord::Base
belongs_to :user
end
user model
class User < ActiveRecord::Base
has_many :submissions, dependent: :destroy
end
The far I have gone is I'm able to get how many submissions a user has using this query
Submission.all.count(:group => "user_id")
With this for example I'm able to get the number of submissions a user with a specific id has
{1=>3, 2=>5}
I want to have a sorted users table with the user with the highest number of submissions first. How can this be achieved in rails activerecord?
You can do what you want in 2 ways:
Using join, group by and order by count
User.select("COUNT(*) AS count_all, submissions.user_id AS submissions_user_id")
.joins('LEFT JOIN submissions ON submissions.user_id = users.id')
.group('submissions.user_id')
.order('COUNT(submissions.user_id) DESC')
This will generate the following sql:
SELECT COUNT(*) AS count_all, submissions.user_id AS submissions_user_id FROM "users" LEFT JOIN submissions ON submissions.user_id = users.id GROUP BY submissions.user_id ORDER BY COUNT(submissions.id) DESC
LEFT JOIN will get the users with 0 submissions too (if you have that situation)
Using counter_cache
The most efficient solutions for querying, in this context, is to use counter_cache
This will enable you to run a query like this:
User.order('submissions_count DESC')
which translates to:
SELECT * FROM users ORDER BY submissions_count DESC
!!! If you want to implement this, especially in production, do a backup of your database before starting. !!!
Read counter_cache docs to understand what it is and how it can help you.
Add a new column on users table named submissions_count.
class AddSubmissionsCountToUsers < ActiveRecord::Migration
def change
add_column :users, :submissions_count, :integer, default: 0
add_index :users, :submissions_count
end
end
Modify your Submission model and add counter_cache.
class Submission < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
If you have a production database update submissions_count to reflect the number of existing submissions:
User.find_in_batches do |group|
group.each do |user|
user_submissions_count = Submission.where(user_id: user.id).count // find how many subscription a user has
user.update_column(:submissions_count, user_submissions_count)
end
end
Every time a user will create/destroy a subscription, submissions_count will be incremented/decremented for that user to reflect the change.

Rails 3 has many through query all parent records and only include children that meet conditions

I have a relationship where restaurants can have many users through a middle table called actions, and users has many restaurants through actions as well.
I am trying to query all restaurants and only include users that meet certain conditions; the restaurant should be returned regardless if there are no users that meet searched criteria, or if there are no users associated with it at all. My models are as follows.
Restaurant model:
has_many :actions
has_many :users, through: :actions
actions model:
belongs_to :user
belongs_to :restaurant
user model:
has_many :actions
has_many :restaurants, through: :actions
What you need to do for this situation is use a left outer join. If you just create the join to find users with as specific attribute like:
Restaurant.joins(:users).where("users.name LIKE 'user_name'")
an inner join is implicitly created which will leave out all of the restaurants without users. To include them do something like this:
Restaurant.joins(["LEFT OUTER JOIN actions on restaurants.id = actions.user_id", "LEFT OUTER JOIN users on users.id = actions.user_id"]).where("users.name LIKE 'James' OR users.name IS NULL ")

Is it possible to define a single SQL query that draws a set of permissible foreign_keys from one table and then uses them to filter another?

Specifically, I'm trying to figure out if it's possible to generate SQL that does what I want to feed into Ruby-on-Rails' find_by_sql method.
Imagine there are Users, who are joined cyclically to other Users by a join table Friendships. Each User has the ability to create Comments.
I'd like a SQL query to return the latest 100 comments created by any friends of a given user so I can display them all in one convenient place for the user to see.
This is tricky, since essentially I'm looking to filter the comments by whether their foreign keys for their author are contained in a set of keys obtained derived from the user's friends' primary keys.
Edit: Clarifying the setup. I'm not exactly sure how to write a schema definition, so I'll describe it in terms of Rails.
class User
has_many :friends, :through => :friendships
has_many :comments
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
end
def Comment
has_one :User
end
It's not that tricky, you just use joins. To get just the comments you only need to join the Friendships table and the Comments table, but you probably also want some information from the Users table for the person who wrote the comment.
This would get the last 100 comments from people who are friends with the user with id 42:
select top 100 c.CommentId, c.CommentText, c.PostDate, u.Name
from Friendships f
inner join Users u on u.UserId = f.FriendUserId
inner join Comments c on c.UserId = u.UserId
where f.UserId = 42
order by c.PostDate desc