はじめに
なぜ書いたか
- whereやpluckの戻り値を理解しておらず、詰まった経験を共有します
- 「
.id
でいけるだろ」と、ActiveRecordを乱用してしまうことに反省の意味を込めて
結論
-
whereやpluckの戻り値は配列である→ pluckは配列で返す - whereは ActiveRecord::Relation を返す ※ 追記20/11/10
- ActiveRecordのメソッドチェインの使い所を理解しよう
前提
環境
・ Ruby 2.6.5
・ Rails 6.0.3
・ devise(userモデル)を使用
ER図
roomsテーブル: DMする部屋を管理します
user_roomsテーブル: userとroomの組み合わせを管理します
やりたいこと
<やりたいこと>
DM相手(id=10) のユーザー情報を取得したい
<条件>
DMするroomは room_id: 2
自分(current_user)は user_id: 15
事象
下記のコードで実現しようとしました
class RoomsController < ApplicationController
def show
@room = Room.find(params[:id])
current_user_room = @room.user_rooms.where.not(user_id: current_user.id)
user_id = current_user_room.user_id
@user = User.find(user_id)
end
end
3行目:2人のユーザーが参加している@room(id:2)を定義します。
4行目:@room.user_rooms
で@roomとユーザーの組み合わせを取得します。(この時点でレコードは2つに絞られます。)さらに、where.not(user_id: current_user.id)
で自分が含まれていない方のレコードに絞り込みます。
5行目:4行目で取得したレコードのうち、user_idを取得しようとしています。
ただし、4行目で以下のようなエラーになります。
[*] pry(main)> current_user_room.user_id
# 結果
NoMethodError: undefined method `user_id' for #<UserRoom::ActiveRecord_AssociationRelation:0x00007fbef39437e8>
???
とりあえず、current_user_room
を出力してみます
[**] pry(main)> current_user_room
# 結果
=> [#<UserRoom:0x00007fbeefb36518
id: 2,
user_id: 10,
room_id: 2,
created_at: Sat, 07 Nov 2020 04:00:28 UTC +00:00,
updated_at: Sat, 07 Nov 2020 04:00:28 UTC +00:00>]
なぜ出来ないんだろう。試しにid=2を使ってfind
で取得してみます
[**] pry(main)> user_room1 = UserRoom.find(2)
# 結果
=> #<UserRoom:0x00007fbeec46c960
id: 2,
user_id: 10,
room_id: 2,
created_at: Sat, 07 Nov 2020 04:00:28 UTC +00:00,
updated_at: Sat, 07 Nov 2020 04:00:28 UTC +00:00>
はじめは同じ結果に見えていましたが、よく見るとcurrent_user_room
の方は[]
がついていて、配列になってます。 → 追記
current_user_room
の方は、where
メソッドで取得していました。
配列に対してメソッドチェインをあてても取れませんね。where
メソッドはデータ群を取るものですから当然です。
ちなみに、pluck
メソッドでuser_id
を取得しようとしましたが同様の結果でした。
解決策
配列から拾ってあげれば解決しそうです。
ということでRailsガイドを見てみると、take
メソッドが使えそうです。取得したいのは1個だと分かっているからです。
takeメソッドはレコードを1つ取り出します。どのレコードが取り出されるかは指定されません。
# 元のコード
current_user_room = @room.user_rooms.where.not(user_id: current_user.id)
# 対応コード(末尾に.takeを追加)
current_user_room = @room.user_rooms.where.not(user_id: current_user.id).take
※ または、末尾に配列の一個目を取得する[0]
を付けても解消します。
修正後のコード
class RoomsController < ApplicationController
def show
@room = Room.find(params[:id])
current_user_room = @room.user_rooms.where.not(user_id: current_user.id).take #.takeを追加
user_id = current_user_room.user_id
@user = User.find(user_id)
@messages = @room.messages.includes(:user)
@message = Message.new
end
end
追記
where
が返すのは配列ではなく、ActiveRecord::Relation
のインスタンスであることが分かりました。
# (where) current_user_room のクラスを確認
pry(main)> current_user_room.class
=> UserRoom::ActiveRecord_AssociationRelation
# (find) user_room1 の方も確認
pry(main)> user_room.class
=> UserRoom(id: integer, user_id: integer, room_id: integer, created_at: datetime, updated_at: datetime)
まだまだ理解が浅いので ActiveRecord
、ActiveRecord::Relation
についてしっかり調べてみようと思います。
おわりに
ひとまず現状で出来る対応策をとりましたが、根っこから見直せばそもそもこの記述すらいらないのかもしれません。気付き次第直していきたいです。
ただし今回は、where
の挙動に触れることが出来て良い勉強になりました。
もっとレベルアップしていきたいので、不備や他の方法があればご指摘いただけると幸いです。
参考資料
Railsガイド
ActiveRecord の find と where の違い。 → 追記で非常に参考にさせて頂きました