Search code examples
ruby-on-railsruby-on-rails-4many-to-manyhas-many-through

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?


Solution

  • 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.