Techioz Blog

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