ユーザー同士がチャットできる機能を実装しています。
メッセージの送信は実装出来ています。
ユーザー同士のメッセージのやりとりの中での、最後のメッセージを効率よく取得する方法を探しています。
LINEやTwitterのチャット一覧をイメージしています。
最後のメッセージと、相手の名前、画像が表示される画面です。
前提条件
ruby
1# routes.rb 2resources :users, only: %i[index show] do 3 resources :messages, only: %i[index create] do 4 collection do 5 get :dashbord 6 end 7 end 8end
ruby
1# user.rb 2class User < ApplicationRecord 3 has_many :from_messages, class_name: 'Message', 4 foreign_key: 'from_id', dependent: :destroy 5 has_many :to_messages, class_name: 'Message', 6 foreign_key: 'to_id', dependent: :destroy 7 8 def send_message(other_user, room_id, content) 9 from_messages.create!(to_id: other_user.id, room_id: room_id, content: content) 10 end 11end
ruby
1# message.rb 2class Message < ApplicationRecord 3 belongs_to :from, class_name: 'User' 4 belongs_to :to, class_name: 'User' 5 6 def self.recent_in_room(room_id) 7 where(room_id: room_id).last(500) 8 end 9end
ruby
1class MessagesController < ApplicationController 2 def index 3 @room_id = message_room_id(current_user.id, params[:user_id]) 4 @messages = Message.recent_in_room(@room_id) 5 end 6 7 def create 8 @message = current_user.from_messages.build(message_params) 9 end 10 11 private 12 def message_room_id(first_user_id, second_user_id) 13 first_id = first_user_id.to_i 14 second_id = second_user_id.to_i 15 if first_id < second_id 16 "#{first_user_id}-#{second_user_id}" 17 else 18 "#{second_user_id}-#{first_user_id}" 19 end 20 end 21end
Messageのカラムは,to_id(送り先のユーザーid), from_id(送信したユーザーid), content(文章), room_id(ユーザーidをハイフンで結合したもの)になっています。
room_idは二人のユーザー間で同じになるように、Messageモデルのmessage_room_idで実装しています。
例えばユーザー1がユーザー2にメッセージを送信した時、そのメッセージのroom_idは1-2となります。
ユーザー2がユーザー1にメッセージを送信した時も、room_idは1-2となります。
このように、room_idを二つのユーザー間で同じにすることで、MessageControllerのindexでメッセージのやりとりを取得できます。
思考錯誤した部分
LINEやTwitterのチャット一覧画面は、どのような動きをしているか考えました。
- 自分が送信したか、相手が送信したかに限らず、やりとりの最後のメッセージが表示される
- やりとりしている相手の名前、画像が表示される
- 上記二つが複数表示される
自分が関わったメッセージを取得するには、room_idを使うことにしました。
ruby
1class MessagesController < ApplicationController 2 def dashbord 3 @my_rooms=Message.where('room_id like ? or room_id like ?', "#{current_user.id}-%", "%-#{current_user.id}") 4 end 5end
ruby
1# dashbordの結果 2=> #<ActiveRecord::Relation [#<Message id: 29, content: "こんにちは", from_id: 1, to_id: 2, room_id: "1-2", created_at: "2020-03-14 06:03:13", updated_at: "2020-03-14 06:03:13">, #<Message id: 30, content: "いい天気ですね", from_id: 2, to_id: 1, room_id: "1-2", created_at: "2020-03-14 06:03:43", updated_at: "2020-03-14 06:03:43">, #<Message id: 38, content: "こんにちは", from_id: 2, to_id: 2, room_id: "2-2", created_at: "2020-03-14 07:45:08", updated_at: "2020-03-14 07:45:08">]>
こんな感じで自分が関わったメッセージを取得できます。
ですが、ここで一つ問題があります。
ユーザー間でのやり取りが複数あった場合、メッセージも複数取得することになります。
自分が実装したいのはroom_idごとに最後のメッセージを取得する機能です。
そうだ!重複を削除すればいいんだ!と考え以下のように絞り込んでみました。
ruby
1@my_rooms=Message.where('room_id like ? or room_id like ?', "#{current_user.id}-%", "%-#{current_user.id}").order(id: DESC).group(:room_id).select(:room_id)
ですが、重複を削除した時点で、絞り込んだMessageのidがnilになってしまい、Messageを特定できずにいます。
このまま重複を削除した状態で、room_idのみを使い再度Messageを探すこともできます。
ruby
1@my_rooms.each do |room| 2 message=Message.where(room_id: room.room_id).last 3 puts message.content 4end
こんな感じでメッセージを取得することもできるのですが、whereが二回も使われてしまうのは、あまりよくないかなと考えています。
メッセージの相手が増えていった場合、読み込みに時間がかかるのは避けたいです。
また最大の問題は、userを取得する時に、
ruby
1@my_rooms.each do |room| 2 message=Message.includes(:to, :from).where(room_id: room.room_id).last 3 puts message.content 4 puts message.to.name 5end
のように書かなくてはいけないことです。
ループの中で、includesしているので、 結局ループの回数分userが読み込まれることになります。
知りたいこと
Messageを絞り込む部分で、もっとスマートに書く方法を探しています。
全く同じではないが、ほとんど似たようなSQLを発行しないようなコードを目指しています。
また、そもそもモデルの構成、root等の前提がよろしくない可能性もあるなと考えています。
そこまで複雑な機能ではないので、作り直しも考えています。
仮にRoomモデルなんかがあったら
ruby
1message=room.messages.last 2message.content
のように手軽に取得できるのに... と思っています。
他に必要なコード、ご質問等ありましたらコメントおねがします。
よろしくお願いします。
あなたの回答
tips
プレビュー