Adam Keenan
Adam Keenan

Reputation: 784

Ruby on Rails - Referencing a many_to_many relationship

So if I have a many_to_many table with just the ids in it, and I want to reference it within another model, how would I do that?

So in the model, which is called Rating, I want to say that it is linked to both of those things in the association, in this case Course and Textbook. So a Rating is defined by the Textbook and the Course. Should I add two id attributes to Rating that link to both? Should I add an ID to the many_to_many and then have an ID attribute in the Rating table? Or is there something else I can do?

UPDATE:

So here is my ERD so far. You can ignore things with Students and Professors. ERD On the right hand side the the table that I made according to @RichPeck. I'm confused on how to create the entries. Lets say for example I create a Course.

course = Course.new(params)

I then do this to make a Textbook.

textbook = course.textbooks.build(params)

Now when I do course.textbooks, it lists the textbook i just made but when I do textbook.courses I get an empty list. Also, after saving them and exiting the shell then reopening, both commands reutnr empty list. Now this is because the join table, CourseTextboks, is empty. Why is rails not adding an entry into there? Do I need to do something special?

Here are the relevant files:

CreateCourseTextbooksMigration

class CreateCourseTextbooks < ActiveRecord::Migration
  def change
    create_table :course_textbooks do |t|
      t.references :course, index: true
      t.references :textbook, index: true

      t.timestamps
    end
  end
end

CreateTextbookMigration

class CreateTextbooks < ActiveRecord::Migration
  def change
    create_table :textbooks do |t|
      t.string :title
      t.string :authors
      t.string :edition
      t.float :price
      t.string :isbn
      t.text :description
      t.string :image_url
      t.date :published

      t.timestamps
    end
  end
end

CreateCourseMigration

class CreateCourses < ActiveRecord::Migration
  def change
    create_table :courses do |t|
      t.string :title
      t.string :letters
      t.integer :number

      t.timestamps
    end
  end
end

And the models:

Course

class Course < ActiveRecord::Base
  has_many :student_courses
  has_many :students, through: :student_courses

  has_many :professor_courses
  has_many :professors, through: :professor_courses

  has_many :textbook_courses
  has_many :textbooks, through: :textbook_courses

  validates_presence_of :title, :letters, :number
end

Textbook

class Textbook < ActiveRecord::Base
  has_many :textbook_courses
  has_many :courses, through: :textbook_courses
end

CourseTextbook

class CourseTextbook < ActiveRecord::Base
  belongs_to :course
  belongs_to :textbook

  has_many :rating_course_textbooks, :class_name => "RatingCourseTextbook" 
  has_many :ratings, :through => :rating_course_textbooks

  validates_presence_of :course
end

Can you help me out, guys?

UPDATE2:

I figured it out after about a half hour of searching and paying more attention to the SQL generated when creating objects.. So when you do a course.build it just returns the object and somehow links it, but not through join table. I found out that you have to save the course NOT the new professor, or just use create.

This is what was happening

>>> course = Course.create(params)
    # INSERT into courses table
>>> professor = course.build(params)

>>> professor.save
    # INSERT into professors table
>>> course.professors
    # [Professor {id:1, blah}]
>>> professor.courses
    # []

And this is what works now

>>> course = Course.create(params)
    # INSERT into courses table
>>> professor = course.build(params)

>>> course.save
    # INSERT into professors table
    # INSERT into professor_courses table
>>> course.professors
    # [Professor {id:1, blah}]
>>> professor.courses
    # [Course {id:1, blah}]

YAY! Now just to see if it all works with my other models, like the RatingsCourseTextbooks

UPDATE3 So after messing around for a little while, I discovered that I was making it too complicated. A rating can only have one CourseTextbook so I didn't need another join table. I just added a CourseTextbook ID to Rating.

New ERD

Thanks for your support

Upvotes: 0

Views: 476

Answers (2)

Richard Peck
Richard Peck

Reputation: 76774

I'm guessing you're new to Rails, so I'll explain how this would work for you here:


ActiveRecord Associations

One of Rails' core features is the ActiveRecord association

ActiveRecord brings the relational database schema to life, allowing you to reference different "related" models from a single query. It's how you can call @user.images etc

ActiveRecord sits above the database layer, and is referenced by your models using the has_many, belongs_to, etc references. If you want to reference your related models, you'll have to use these, like this:

#app/models/user.rb
Class User < ActiveRecord::Base
    has_many :images
end 

#app/models/image.rb
Class Image < ActiveRecord::Base
    belongs_to :user
end

Your Code

Seems you're using has-and-belongs-to-many

As mentioned by Sachin Singh, you'll likely be best using a polymorphic relationship with these models: Polymorphic Association

This allows you to reference the relationship from multiple models, with Rails / ActiveRecord populating the polymorphic fields with the model name & id. Here's how your models may look:

Class Rating < ActiveRecord::Base
    belongs_to :rateable, :polymorphic => true
end

Class Course < ActiveRecord::Base
    has_many :ratings, :as => :rateable
end

Class Textbook < ActiveRecord::Base
   has_many :ratings, :as => :rateable
end

Update

If you wanted to have specific textbooks for certain courses, and have ratings for those textbooks, you'll probably best use has_many :through

This might be completely off the mark, but I'd set up the courses and textbooks tables as "top-level" tables, and then have textbook_courses as a join-model. I'd then have textbook_course_ratings as another join model to give the required ratings per textbook

The goal will be @course.textbooks.ratings

Class Course < ActiveRecord::Base
    has_many :textbook_courses
    has_many :textbooks, :through => :textbook_courses
end

Class Textbook < ActiveRecord::Base
   has_many :textbook_courses
   has_many :courses, :through :textbook_courses
end

Class TextbookCourse < ActiveRecord::Base
   belongs_to :textbook
   belongs_to :course

   has_many :textbook_course_ratings, :class_name => "TextbookCourseRating" 
   has_many :ratings, :through => :textbook_course_ratings
end

Class TextbookCourseRating < ActiveRecord::Base
    belongs_to :textbook_course
    belongs_to :rating
end

Class Rating < ActiveRecord::Base
    has_many :textbook_course_ratings, :class_name => "TextbookCourseRating"
    has_many :textbooks, :through => :textbook_ratings
end

I've never done such a deep has_many :through relationship before; but it should work

There several important things to note:

  1. You'll have to change your join tables to include a primary key (id) - HABTM doesn't need them, but HMT does
  2. You'll have a lot of accepts_nested_attributes_for to do :)

Your join tables for this would look like:

textbook_courses
id | course_id | textbook_id | created_at | updated_at

textbook_courses_ratings
id | textbook_course_id | rating_id | created_at | updated_at

Upvotes: 2

Sachin Singh
Sachin Singh

Reputation: 7225

if, A Course and TextBook has many ratings, then ratings table should be polymorphic.

class Rating < AcrtiveRecord::Base    
  belongs_to :ratable, polymorphic: true    
end

class Course < ActiveRecord::Base
  has_many :ratings, as: :ratable
end

class TextBook < ActiveRecord::Base
  has_many :ratings, as: :ratable
end

ratings table will have two column named ratable_type and ratable_id

ratable_type : it will hold class name Course or Textbook.

ratable_id : it will hold id of associated abject.

Upvotes: 0

Related Questions