Rails での多対多の関係を持つ 2 つのモデルの結合
概要
次のモデル間に多対多の関係がある Ruby on Rails API があります。
class Course < ApplicationRecord
has_many :student_courses
has_many :students, through: :student_courses
end
class Student < ApplicationRecord
has_many :student_courses
has_many :courses, through: :student_courses
end
class StudentCourse < ApplicationRecord
belongs_to :student
belongs_to :courses
end
次の形式で json を提供したいと考えています。
[
{
"course": "English",
"students": [
"John",
"Sarah"
]
},
{
"course": "Maths",
"students": [
"John",
"Ella",
"Lee"
]
},
{
"course": "Classics",
"students": [
"Una",
"Tom"
]
}
]
現時点では、ループを使用してこれを行っています。
def index
@courses = Course.all
output = []
@courses.each do |course|
course_hash = {
course: course.name,
students: course.students.map { |student| student.name }
}
output << course_hash
end
render json: output.to_json
end
アクティブ レコード オブジェクトのリレーショナル マッピングを使用してこれを行うより効率的な方法はありますか?
解決策
あなたの例では、Course.all.each を反復し、各反復内で course.students を呼び出すと、N+1 の問題が発生します。つまり、すべてのコースを取得するには 1 つのデータベース クエリがあり、リスト内の個々のコースの学生をロードするには N 個の追加のデータベース クエリが必要になります。
N+1 クエリを回避するために、Ruby on Rails では、インクルードを使用して 1 つまたは最大 2 つのクエリで学生をコースと一緒に一括ロードできます。
もう 1 つの最適化は、配列をそれぞれ反復して変換されたデータを新しい配列にコピーするのではなく、Enumerable#map で既存の配列を再利用することでメモリ消費を削減することです。
それを一緒に入れて:
def index
courses_with_students = Course.includes(:students).map do |course|
{ course: course.name, students: course.students.map(&:name) }
end
render json: courses_with_students.to_json
end