はじめに
おはこんばんにちは。お疲れ様です。
今回は、多対多関係にあるレコードで
紐づいている関連先レコードの情報で条件判定を行い、一致した関連元レコードを取得する
といったことを行いましたので備忘録として残しておこうと思います。
環境
version | |
---|---|
Ruby | 3.0.3 |
Rails | 6.1.4 |
前提
以下の関係性をもつテーブルがあるとします。
題材として大学を想定します。
User(学生)テーブルとLecture(講義)テーブルがあり、中間テーブルとしてUserLectureテーブルがあるとします。
要件
今回行いたいのは、
特定の複数の講義をすべて受講している、登録が新しい学生を取得したいと思います。
講義は、英語、ドイツ語、フランス語、イタリア語とします。
要するに、上の4つの言語に関する講義をすべて受講している入学して間もない大学生
を探したいと思います。
結論
以下のように実装を行いました。
# lecture_ids = Lecture
# .where(name: ["英語", "ドイツ語", "フランス語", "イタリア語",])
# .pluck(:id)
# 対象のlectureを受講しているuserを絞り込み取得する
def find_user_by_taking_all_lectures(lecture_ids)
# 対象のlectureを受講しているuserを絞り込み
users = User.order(created_at: :DESC)
.joins(:lectures)
.where(lectures: {id: lecture_ids})
# なければ早期終了
return if users.blank?
# lectureが1件しかなければ上記の検索処理のみで取得可能なため早期終了
return users.first if lecture_ids.count == 1
# 指定されたlecture全てを受講しているユーザーIDを取得
target_user_ids = users.pluck(:id)
.group_by(&:itself)
.transform_values(&:count)
.select{|_, count| count == lectures_count}
.keys
# 最初のユーザーを取得して返す
return users.find_by(id: target_user_ids.first)
end
解説
流れは以下のようになっています。
- Userを降順で並び替えて、
lecture_ids
で絞り込む -
lecture_ids
の数でフィルターする - フィルターした中から最初の値でUser検索
1. Userを並び替えて絞り込み
以下の部分ですね。
users = User.order(created_at: :DESC)
.joins(:lectures)
.where(lectures: {id: lecture_ids})
SQL的には以下のようになりますね。
SELECT "users".* FROM "users"
INNER JOIN "user_lectures"
ON "user_lectures"."user_id" = "users"."id"
INNER JOIN "lectures"
ON "lectures"."id" = "user_lectures"."lecture_id"
WHERE "lectures"."id" IN (1, 2, 3, 4) -- メソッドの引数として渡された指定されたlecture id
ORDER BY users.created_at DESC
例えば、IDが「3」のuserは4つの講義を受けているとします。
次に、IDが「2」のuserは4つのうち、3つの講義を受けていて、
IDが「1」のuserは4つのうち、2つだけ講義を受けているとします。
この場合、出力結果をIDだけにすると、以下のようになります。
# 1の結果
[3, 3, 3, 3, 2, 2, 2, 1, 1]
IDが「3」のuserは、4つすべてを受講しているため4つ出力されます。
同様に、IDが「2」のuserは3つ、
IDが「1」のuserは2つ出力されます。
これをうまくやろうというわけです。
2.講義数でフィルターする
以下の部分ですね。
target_user_ids = users.pluck(:id)
.group_by(&:itself)
.transform_values(&:count)
.select{|_, count| count == lectures_count}
.keys
出力結果は以下のようになります。
# 2の結果
[3]
過程としては、まず重複しているIDをグループ化してその数を数えます。
# users.pluck(:id)
# .group_by(&:itself)
# .transform_values(&:count)
# ID=>count
{3=>4, 2=>3, 1=>2}
今回は、指定した講義すべて(4つ)を受講しているUserが欲しい。
つまり、4回出力されている値を取得すればよいということになります。
# .select{|_, count| count == lectures_count}
# lectures_countは4
{3=>4}
# .keys
[3]
よって、「3」という値が取得できます。
3.フィルターした中から最初の値でUser検索
以下の部分ですが、さくっと検索して終わりです。
# 2の結果より、target_user_ids = [3]
# 必ず1つの値とは限らないので、
# すでにorderされている2の結果から最初の値を使用
users.find_by(id: target_user_ids.first)
最後に
やっていることは単純ですが、はじめに実装するときは悩みました。
また、パフォーマンスも担保しつつということで、記述した通りになりましたが
よりよい方法があれば教えていただきたいです。
なんかの役に立つことを願って、また次の記事で会いましょう