How to make a joint fetch? - ruby-on-rails

I am building a Rails 5.2 app.
In this app I got three objects:
User
Id
Firstname
Lastname
Conversation
Id
Entity_type
Entity_subtype
Assignment
Id
User_id
Assignable_id (ID of Conversation)
Assignable_type (Class name of Conversation)
One or more Users are connected to a Conversation through Assignments.
I am trying to create a query that checks if two Users are having a direct message Conversation or I need to create a new one automatically.
I have this code and it "works" (I get no error) but it returns a Conversation even though only one of the Users are involved in a Conversation. But I need it to only return a Conversation if Both Users share the Same Conversation, ie they are talking to each other.
Conversation.where(entity_type: "conversation", entity_subtype: "direct").joins(:assignments).where(:assignments => {:user_id => ["cdd3c6be-ac78-46f2-a7ae-7f2299b6fedb", "32117e53-9b2f-49c6-8cc8-3a9eb9003a2e"] })

One way to do this is by using GROUP and using HAVING to set a condition on the group:
class Conversion < ActiveRecord::Base
def self.between(*users)
joins(:assignments)
.group(:id)
.where(assignments: { user_id: users })
.having(Assignment.arel_table[:id].count.eq(users.length))
end
end
The advantage is that this approach can be used with any number of users.
If you change out .eq to .gteq you can get conversions that include the two users but isn't a private conversation. On Postgres you can use Assignment.arel_table[Arel.star] as an optimization.

Join assignments twice and in resulting rows, pick a row with assignment user ids match.
user_ids = ["cdd3c6be-ac78-46f2-a7ae-7f2299b6fedb", "32117e53-9b2f-49c6-8cc8-3a9eb9003a2e"]
Conversation
.where(entity_type: "conversation", entity_subtype: "direct")
.joins("LEFT JOIN assignments as1 ON assignments.assignable_id = conversations.id AND assignments.assignable_type = 'Conversation'")
.joins("LEFT JOIN assignments as2 ON assignments.assignable_id = conversations.id AND assignments.assignable_type = 'Conversation'")
.where('as1.user_id = as2.user_id')
.where('as1.user_id = ? AND as2.user_id = ?', user_ids[0], user_ids[1])
Will give you list of convos, both are involved

Related

How to Sort Record in Ruby on Rails based on Last Record Timestamp from References Table

I need to create a live chat app and now I have three models :
ChatRoom
ChatRoomMember
ChatRoomMessage
From this three models, I use includes and references to get the list of chat_rooms for current login user. Here is the code that I have wrote.
#chat_rooms = ChatRoom.includes(:members).references(:members)
#chat_rooms = #chat_rooms.includes(:messages).references(:messages)
#chat_rooms = #chat_rooms.where 'chat_room_members.user_id = ?', #current_user.id
#chat_rooms = #chat_rooms.order 'chat_room_messages.created_at DESC'
#chat_rooms = #chat_rooms.limit(limit).offset(offset)
However, the order didn't work as I expected. What I want is that the chat_rooms are sorted by created_at column from the last message in that room. How can I do this ?
Here is the database structure :
Use association to avoid where 'chat_room_members.user_id = ?', #current_user.id
Here is my suggestion, assuming User has associations looking like:
class User
has_many :chat_room_members
has_many :chat_rooms, through: :chat_room_members
end
# list only rooms with at least on message
#chat_rooms = #current_user.chat_rooms.joins(:messages).order('chat_room_messages.created_at DESC').limit(limit).offset(offset)
# list all rooms even if there is no message attached
#chat_rooms = #current_user.chat_rooms.distinct.left_joins(:messages).order('chat_room_messages.created_at DESC').limit(limit).offset(offset)
Try this:
ChatRoom.includes(:messages).order('chat_room_messages.created_at DESC')
Thanks for everyone has help me to solve this probel. I have my own answer. Here is it.
SELECT chat_rooms.*, (SELECT chat_room_messages.created_at FROM chat_room_messages WHERE chat_room_messages.chat_room_id = chat_rooms.id ORDER BY chat_room_messages.id DESC LIMIT 1) AS last_message_at FROM chat_rooms INNER JOIN chat_room_members ON chat_room_members.chat_room_id = chat_rooms.id WHERE chat_room_members.user_id = #{#current_user.id} ORDER BY last_message_at DESC
I use raw query for solve this problem. Hope this can help anyone who need it !

Select records all of whose records exist in another join table

In the following book club example with associations:
class User
has_and_belongs_to_many :clubs
has_and_belongs_to_many :books
end
class Club
has_and_belongs_to_many :users
has_and_belongs_to_many :books
end
class Book
has_and_belongs_to_many :users
has_and_belongs_to_many :clubs
end
given a specific club record:
club = Club.find(params[:id])
how can I find all the users in the club who have all books in array of books?
club.users.where_has_all_books(books)
In PostgreSQL it can be done with a single query. (Maybe in MySQL too, I'm just not sure.)
So, some basic assumptions first. 3 tables: clubs, users and books, every table has id as a primary key. 3 join tables, books_clubs, books_users, clubs_users, each table contains pairs of ids (for books_clubs it will be [book_id, club_id]), and those pairs are unique within that table. Quite reasonable conditions IMO.
Building a query:
First, let's get ids of books from given club:
SELECT book_id
FROM books_clubs
WHERE club_id = 1
ORDER BY book_id
Then get users from given club, and group them by user.id:
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = 1
GROUP BY CU.user_id
Join these two queries by adding having to 2nd query:
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(##1##)
where ##1## is the 1st query.
What's going on here: Function array_agg from the left part creates a sorted list (of array type) of book_ids. These are books of user. ARRAY(##1##) from the right part returns the sorted list of books of the club. And operator #> checks if 1st array contains all elements of the 2nd (ie if user has all books of the club).
Since 1st query needs to be performed only once, it can be moved to WITH clause.
Your complete query:
WITH club_book_ids AS (
SELECT book_id
FROM books_clubs
WHERE club_id = :club_id
ORDER BY book_id
)
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = :club_id
GROUP BY CU.user_id
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(SELECT * FROM club_book_ids);
It can be verified in this sandbox: https://www.db-fiddle.com/f/cdPtRfT2uSGp4DSDywST92/5
Wrap it to find_by_sql and that's it.
Some notes:
ordering by book_id is not necessary; #> operator works with unordered arrays too. I just have a suspicion that comparison of ordered array is faster.
JOIN users U ON U.id = CU.user_id in 2nd query is only necessary for fetching user properties; in case of fetching user ids only it can be removed
It appears to work by grouping and counting.
club.users.joins(:books).where(books: { id: club.books.pluck(:id) }).group('users.id').having('count(*) = ?', club.books.count)
If anyone knows how to run the query without intermediate queries that would be great and I will accept the answer.
This looks like a situation where you'd make two queries, one to get all the ids you need, the other select perform a WHERE IN.

How to send email to Users who have same interest (Insterts_Users) as category type of Treatment?

I want to email users every time a new treatment that is under the category they are interested in is added.
I have a join table interests_users which is holds a user_id and a interest_id. When a user signs up he/she checks the boxes they are interested in i.e Face, Body etc. Treatments are submitted under the same headings so they can be compared to interests.
What kind of query do I need to write in order that ever time a new treatment is saved users who share the same interest as the category type of treatment get an email?
Inside your create action for TreatmentsController you can iterate over the users using find_by_sql and an appropariate sql command, getting all records that match the interest. Then us a Mailer method as described here ... http://guides.rubyonrails.org/action_mailer_basics.html
The iteration might look like...
if #treatment.save
interest = Interest.find_by(name: #treatment.category)
if interest
User.find_by_sql("SELECT * FROM users
INNER JOIN interests_users ON clients.id = interests_users.client_id
WHERE interests_users.interest_id = ?", interest.id).each do |user|
AppMailer.send_notification(user, #treatment).deliver
end
end
end

rails join on association and id

I want to achieve something that takes exactly 3 seconds on SQL, and I'm struggling with it for hours, I want to load all records and left join if it exists, if not, then don't give me the associated model.
the query I want to create is as follows:
"SELECT * FROM apartments LEFT JOIN comments ON apartments.id = comments.apartment_id AND comments.user_id = ?"
and when I call apartment.comments, it'll give me just the record (can only be one) for the specific user, not all the records for every user.
I tried
Apartment.joins("LEFT OUTER JOIN comments ON comments.apartment_id = apartments.id AND comments.user_id = #{user_id}")
but it doesn't work, as when I call apartments.comments it fires another query which returns all possible comments.
Apartment.includes(:comments).where("comments.user_id = ?", user_id)
doesn't work aswell, because it returns only apartments who has a comment from the specific user.
help is needed!
Maybe you could try this:
#app/models/apartment.rb
Class Apartment < ActiveRecord::Base
has_many :comments
scope :user, ->(id) { where("comments.user_id = ?", id }
end
#apartment = Apartment.find(params[:id])
#comments = #apartment.comments.user(user_id)

ActiveRecord commands sequence to count associated records from different tables

I have Client, User and Action models.
Client belongs to User through has_and_belongs_to_many association. Action belongs to Client. I need to retrieve Clients belonging to specific user(id=1) and count associated with the Client Actions of particular type(action_type=1)
I have the following SQL query, which does the job pretty well
SELECT clients.*, count(CASE WHEN actions.action_type = 1 THEN 1 END) AS client_actions FROM clients, actions, clients_users WHERE clients.id=actions.client_id AND clients.id=clients_users.client_id AND clients_users.user_id=1 GROUP BY clients.id
Which effectively queries database for clients data, and counting associated objects. However I'm struggling to understand how to wrap it into ActiveRecord commands sequence, so I could use Paginate for the results. (at the moment I run the query using find_by_sql)
One potential option would be to add something like this on your Client model:
def self.find_clients(user_id, action_type)
clients = Client.scoped({})
clients = clients.scoped(joins: :users, conditions: ["user_id = ?", user_id] )
clients = clients.scoped(joins: :actions, conditions: ["action_type = ?", action_type] )
clients = clients.count
end

Resources