Arash Hadipanah
Arash Hadipanah

Reputation: 145

Rails - modeling multiple many to many relationships

I have the following use cases for creating an app that handles courses;

  1. Class A is taught by Curt in Bos on 11/1
  2. Class A is taught by Curt in NY on 10/19
  3. Class A is taught by Jane in SF on 12/5
  4. Class A is taught by Jane in Bos on 11/1

What's the best way to create models with many to many relationships for this app?

Should the app have a teachings model that belongs to courses, teachers, and locations with a column for the date?

Upvotes: 3

Views: 160

Answers (2)

max
max

Reputation: 102174

What you want is to create a model for each entity:

  • Course
  • Teacher
  • Location

You then create a join model of sorts which I have choosen to call Lesson:

class Course < ActiveRecord::Base
  has_many :lessons
  has_many :locations, through: :lessons
  has_many :teachers, through: :lessons
end

class Lesson < ActiveRecord::Base
  belongs_to :course
  belongs_to :teacher
  belongs_to :location
end

class Teacher < ActiveRecord::Base
  has_many :lessons
  has_many :courses, through: :lessons
end

class Location < ActiveRecord::Base
  has_many :lessons
  has_many :courses, through: :lessons
  has_many :teachers, through: :lessons
end

I've been playing with this structure for the models but what I noticed is that when submitting the course with a fields_for :locations and a fields_for :instructors, the associations table is creating two separate entries for course_id + instructor_id, course_id + location_id, I would expect a single entry for course_id, instructor_id, location_id. Any thoughts as to why that might happen?

ActiveRecords only ever keeps track of one assocation when you create join models implicitly. To do three way joins you need to create the join model explicitly.

<%= form_for(@course) do |f| %>

  <div class="field>
    <% f.label :name %>
    <% f.text_field :name %>
  </div>

  <fieldset>
    <legend>Lesson plan<legend>
    <%= f.fields_for(:lessons) do |l| %>
      <div class="field>
         <% l.label :name %>
         <% l.text_field :name %>
      </div>
      <div class="field">
         <% l.label :starts_at %>
         <% l.datetime_select :starts_at %>
      </div>
      <div class="field">
         <% l.label :teacher_ids %>
         <% l.collection_select :teacher_ids, Teacher.all, :id, :name, multiple: true %>
      </div>
      <div class="field">
         <% l.label :location_id %>
         <% l.collection_select :location_id, Location.all, :id, :name %>
      </div>
    <% end %>
  </fieldset>
<% end %>

fields_for and accepts_nested_attributes are powerful tools. However passing attributes nested several levels down can be seen as an anti-pattern of sorts since it creates god classes and unexpected complexity.

A better alternative is to use AJAX to send separate requests to create teachers and locations. It gives a better UX, less validation headaches and better application design.

Upvotes: 2

Cyzanfar
Cyzanfar

Reputation: 7136

You are on the right track. Here is how I would model these relationships. Let's say you have a Teacher model, a Course model and a TeacherCourses model that will be our join table between teachers and courses:

class Teacher < ActiveRecord::Base
 has_many :courses, through: :teacher_courses
end

class Course < ActiveRecord::Base
 has_many :teachers, through: :teacher_courses
end

class TeacherCourse < ActiveRecord::Base
  belongs_to :course
  belongs_to :teacher
end

Your teacher_courses table would also have location attribute differentiating a record from the same course/teacher combo:

create_table :teacher_courses do |t|
  t.integer :teacher_id
  t.integer :course_id
  t.string :location
  t.timestamps
end

Upvotes: 2

Related Questions