Search code examples
ruby-on-railsrubyactiverecordmany-to-manyobject-relational-model

Joining two models with many-to-many relationship in Rails


I have a ruby on rails api which has a many to many relationship between the following models:

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

I want to serve json in the following format:

[
  {
    "course": "English",
    "students": [
      "John",
      "Sarah"
    ]
  },
  {
    "course": "Maths",
    "students": [
      "John",
      "Ella",
      "Lee"
    ]
  },
  {
    "course": "Classics",
    "students": [
      "Una",
      "Tom"
    ]
  }
]

At the moment I'm doing this using a loop:

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

Is there a more efficient way to do this using active record object relational mapping?


Solution

  • In your example, iterating Course.all.each and then calling course.students within each iteration will lead to an N+1 problem. Which means there will be one database query to get all courses and the N additional database queries to load the students for each individual course in the list.

    To avoid N+1 queries, Ruby on Rails allows to eager load students together with the courses in one or maximum two queries by using includes

    Another optimization could be to reduce memory consumption by reusing the already existing array with Enumerable#map instead of iterating the array with each and coping the transformed data into a new array.

    Putting it together:

    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