This page looks best with JavaScript enabled

【Rails】index_byとgroup_byを用いて取り回しのきくハッシュを作成する

 ·   ·  ☕ 3 分で読めます
✏️

要件の例

Question -< Answer >- User

question has_many answers
user has_many answers

問題があらかじめ用意されていて、ユーザーはそれぞれ問題に解答する(1問につき1解答)。
このとき、以下のようなページを作りたい。

  • 問題一覧を表示する
  • 問題に解答済みの場合は、自分の解答を表示する

image

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|

こんな感じです。

Share on

END
END
@aiandrox

目次