要件の例
Question -< Answer >- User
question has_many answers
user has_many answers
問題があらかじめ用意されていて、ユーザーはそれぞれ問題に解答する(1問につき1解答)。
このとき、以下のようなページを作りたい。
- 問題一覧を表示する
- 問題に解答済みの場合は、自分の解答を表示する
find_by
を使ってレコードを取得する(アンチパターン)
素直に実装すると以下のようになる。
1
2
3
4
5
|
class QuestionsController < ApplicationController
def index
@questions = Question.all
end
end
|
1
2
|
- @questions.each do |question|
- answer = question.answers.find_by(user_id: current_user.id) # これを取得したい
|
こんな感じで「それぞれの問題に対する解答レコード」を取得できる。
しかし、answerを取得するためにfind_by
を使っているので、その度にクエリを発行してしまう(以下のログの3行目以降)。
1
2
3
4
5
6
7
8
|
Question Load (0.6ms) SELECT `questions`.* FROM `questions` ORDER BY `questions`.`id` ASC
↳ app/views/mypage/questions/index.html.slim:11
Answer Load (0.5ms) SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` = 29 AND `answers`.`user_id` = 14 LIMIT 1
↳ app/views/mypage/questions/index.html.slim:30
Answer Load (0.6ms) SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` = 30 AND `answers`.`user_id` = 14 LIMIT 1
↳ app/views/mypage/questions/index.html.slim:30
Answer Load (0.6ms) SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` = 54 AND `answers`.`user_id` = 14 LIMIT 1
↳ app/views/mypage/questions/index.html.slim:30
|
とりあえず、each内でクエリを発行する実装は危険信号と思っておけばいい。
こんなクエリを阻止するために、使用するレコードを事前にハッシュの形に加工しておく。
index_by
を使ってレコードを取得する
1
2
3
4
5
6
|
class QuestionsController < ApplicationController
def index
@questions = Question.all
@question_answer_hashes = Answer.where(question: @questions, user: current_user).index_by(&:question)
end
end
|
1
2
|
- @questions.each do |question|
- answer = @question_answer_hashes[question]
|
1
2
3
4
5
6
7
|
Answer Load (0.7ms) SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` IN (SELECT `questions`.`id` FROM `questions` ORDER BY `questions`.`id` ASC) AND `answers`.`user_id` = 14
↳ app/controllers/mypage/questions_controller.rb:15:in `index`
Question Load (0.5ms) SELECT `questions`.* FROM `questions` WHERE `questions`.`id` IN (29, 30)
↳ app/controllers/mypage/questions_controller.rb:15:in `index`
Rendering mypage/questions/index.html.slim within layouts/mypage
Question Load (0.6ms) SELECT `questions`.* FROM `questions` ORDER BY `questions`.`id` ASC
↳ app/views/mypage/questions/index.html.slim:11
|
@question_answer_hashes
の中身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
[1] pry(#<#<Class:0x00007ff39eeae810>>)> @question_answer_hashes
=> {#<Question:0x00007ff39f22ae58
id: 29,
title: "問題1",
created_at: Mon, 29 Mar 2021 19:07:40 JST +09:00,
updated_at: Mon, 29 Mar 2021 19:32:24 JST +09:00>=>
#<Answer:0x00007ff39ee64648
id: 46,
question_id: 29,
user_id: 14,
comment: "解答です",
created_at: Tue, 13 Apr 2021 20:51:59 JST +09:00,
updated_at: Tue, 13 Apr 2021 21:07:20 JST +09:00>,
#<Question:0x00007ff39f22ad18
id: 30,
title: "問題2",
created_at: Mon, 29 Mar 2021 19:07:50 JST +09:00,
updated_at: Mon, 29 Mar 2021 19:32:37 JST +09:00>=>
#<Answer:0x00007ff39f5aff60
id: 48,
question_id: 30,
user_id: 14,
comment: "解答ですよ",
created_at: Mon, 19 Apr 2021 16:18:37 JST +09:00,
updated_at: Mon, 19 Apr 2021 16:18:37 JST +09:00>}
|
簡単に書くと、
1
2
3
4
|
{
questionA => answerA,
questionB => answerB
}
|
という感じ。なので、@question_answer_hashes[question]
でAnswerを取得できる。
ちなみに、1問につきユーザーが解答できるのが1つまでだとindex_by
でいいが、1問につき何問も解答できる場合はgroup_by
を使う。
group_by
を使った実装
ユーザーの解答が1問につき複数存在し得る(IPPONグランプリ形式)ので、ハッシュのvalueが配列になる。
1
2
3
4
5
6
|
class QuestionsController < ApplicationController
def index
@questions = Question.all
@question_answer_hashes = Answer.where(question: @questions, user: current_user).group_by(&:question)
end
end
|
1
2
3
4
|
- @questions.each do |question|
- answers = @question_answer_hashes[question]
- if answers.present?
- answers.each do |answer|
|
こんな感じです。
個数をハッシュにまとめたい場合は、以下のようにできる。
1
2
|
@questions = Question.all
@question_answer_count_hashes = Answer.where(question: @questions, user: current_user).group(:user_id).count
|