3

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?

2 Answers2

2

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
Cyzanfar
  • 6,997
  • 9
  • 43
  • 81
  • I would still use a seperate table for locations for data normalization. – max Oct 16 '17 at 19:15
  • Not sure i'm following. How would that be beneficial? I'm sure you're right just curious about the tradeoff – Cyzanfar Oct 16 '17 at 19:18
  • 1
    Well instead of having houndreds of strings with different variations on `D-Wing, University of Derpville, Derpville, DERPLAND` you point to a single normalized record. It also makes it straight forward to add features like for example geolocation. – max Oct 16 '17 at 19:24
  • @Cyzanfar the purpose is to be able to reuse a location as it can have many courses. Your solution seems to suggest that the location would have to be entered in each time a course is created. – Arash Hadipanah Oct 16 '17 at 19:58
2

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.

max
  • 96,212
  • 14
  • 104
  • 165
  • thanks for this - 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? – Arash Hadipanah Oct 16 '17 at 19:20
  • Because you're doing it wrong. When the joining model is not just a 2-way join model you cannot create it implicitly as ActiveRecord only can keep track of one relation. See my edit. – max Oct 16 '17 at 19:30
  • thanks for the info, I wasn't aware Rails had limitations with three way joins. So the only way to do this is to create the location and teacher separately and then associate them using a collection select? – Arash Hadipanah Oct 16 '17 at 19:55
  • Yes. You can nest `fields_for(:teachers)` inside `fields_for(:lessons)` but it is a anti-pattern. – max Oct 16 '17 at 20:08
  • Interesting, I will play around with your recommendation of making this a three step process. For including dates of each course, would you recommend adding a column to the lessons table? – Arash Hadipanah Oct 16 '17 at 20:29
  • Nicely done I get the point now of separating these into discrete objects that can then be associated with each other. Thanks for the feedback guys! – Cyzanfar Oct 17 '17 at 01:35