2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Rails]関連レコードの一致条件による対象レコードの検索

Last updated at Posted at 2024-12-25

はじめに

おはこんばんにちは。お疲れ様です。

今回は、多対多関係にあるレコードで
紐づいている関連先レコードの情報で条件判定を行い、一致した関連元レコードを取得する
といったことを行いましたので備忘録として残しておこうと思います。

環境

version
Ruby 3.0.3
Rails 6.1.4

前提

以下の関係性をもつテーブルがあるとします。

スクリーンショット 2024-12-25 182922.png

題材として大学を想定します。
User(学生)テーブルとLecture(講義)テーブルがあり、中間テーブルとしてUserLectureテーブルがあるとします。

要件

今回行いたいのは、
特定の複数の講義をすべて受講している、登録が新しい学生を取得したいと思います。

講義は、英語、ドイツ語、フランス語、イタリア語とします。

要するに、上の4つの言語に関する講義をすべて受講している入学して間もない大学生を探したいと思います。

結論

以下のように実装を行いました。

model.rb
# 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

解説

流れは以下のようになっています。

  1. Userを降順で並び替えて、lecture_idsで絞り込む
  2. lecture_idsの数でフィルターする
  3. フィルターした中から最初の値で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)

最後に

やっていることは単純ですが、はじめに実装するときは悩みました。

また、パフォーマンスも担保しつつということで、記述した通りになりましたが
よりよい方法があれば教えていただきたいです。

なんかの役に立つことを願って、また次の記事で会いましょう

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?