2

I'm a fairly new to rails, and I'm trying to create an app where users can own many classrooms, and classrooms can have many users. I'm not sure how to set up the models, however.

Let’s say I have a teacher named Joe. Joe has many classrooms. Each of Joe’s classrooms can have many students, so from those many classrooms Joe also has many students. But I also want Joe to be able to be part of a classroom as a student, and I want students to be able to create their own classrooms, as teachers, from the same account.

Classrooms should also be able to have more than one teacher.

I also want to be able to do something like user.classrooms.first.users, where user is Joe, and users are his students in his first classroom.

What models should I create, and how would I set up the associations?

I was thinking has_and_belongs_to_many, but apparently it's not preferred anymore, and has_many :through would be better. What it's looking like to me is something like:

class User
  has_many :classroom_users
  has_many :classrooms, :through => :classroom_users
end

class Classroom
  has_many :classroom_users
  has_many :users, :through => :classroom_users
end

class Classroom_User
  belongs_to :classroom
  belongs_to :user
end

Is this correct? I took it from here. Also, how should I mirror these models in the database with migrations?

liver
  • 498
  • 4
  • 21

1 Answers1

2

What you've got at the moment will work for the case of associating users with classrooms, but not teachers as well, since at the moment the table to associate the two models can only signify one type of relationship.

NB: Rails expects model names to be singular and without underscores i.e. ClassroomUser instead of Classroom_Users in your example.

To associate teachers with classrooms, one approach would be to create an extra joining model:

user.rb:

class User < ActiveRecord::Base
  has_many :classroom_teachers
  has_many :classroom_students
  has_many :teaching_classrooms, through: :classroom_teachers
  has_many :attending_classrooms, through: :classroom_students
end

classroom.rb:

class Classroom < ActiveRecord::Base
  has_many :classroom_teachers
  has_many :classroom_students
  has_many :teachers, through: :classroom_teachers
  has_many :students, through: :classroom_students
end

classroom_student.rb:

class ClassroomStudent < ActiveRecord::Base
  belongs_to :student, class_name: 'User', foreign_key: 'user_id'
  belongs_to :attending_classroom, class_name: 'Classroom', foreign_key: 'classroom_id'
end

classroom_teacher.rb:

class ClassroomTeacher < ActiveRecord::Base
  belongs_to :teacher, class_name: 'User', foreign_key: 'user_id'
  belongs_to :teaching_classroom, class_name: 'Classroom', foreign_key: 'classroom_id'
end

Rails normally works out what type of model relates to a field based on the name of the field, e.g. a users field will link to a collection of User models. With the above schema, Rails can't infer the types of the models from the names of the associating fields, since it doesn't know that a teacher is an alias for a user. To overcome this, the class_name attributes define the model types of the joining fields.

For the same reason, Rails needs some guidance knowing what database key relates to which field, which is what the foreign_key attribute is for.

And finally the migration:

class CreateClassroomUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
    end

    create_table :classrooms do |t|
    end

    create_table :classroom_students do |t|
      t.belongs_to :user, index: true
      t.belongs_to :classroom, index: true
    end

    create_table :classroom_teachers do |t|
      t.belongs_to :user, index: true
      t.belongs_to :classroom, index: true
    end
  end
end

Edit:

Alternatively, instead of using two joining models, you could add an extra field to the ClassroomUser model that you originally had, describing the role of the user (e.g. an enum that can be either student or teacher). That would allow more roles to be added in the future and might be easier to query than my previous suggestion. For example, to check whether a user is a student or teacher you'd need only one query:

example_user.classroom_users

and could then check the role fields on the returned ClassroomUser records. See this question for an example of that approach.

Community
  • 1
  • 1
tmw
  • 472
  • 4
  • 11
  • Thank you so much! Really helped! – liver Nov 28 '15 at 21:23
  • No problem. I've made a small edit at the end of my post, since I'm not sure my original answer was the best solution to what you're trying to model. – tmw Nov 28 '15 at 21:40
  • If I wanted to add an already existing user to an already existing classroom, how would I go about doing that? I've tried `Classroom_Student.create!(user_id: example_user, classroom_id: example_classroom)` but it doesn't work. This is what I get: `Unable to autoload constant Classroom_Student, expected app/models/classroom_student.rb to define it`. – liver Nov 29 '15 at 05:26
  • You should get rid of the underscore and use `ClassroomStudent` (See the second paragraph of my answer). Then you can create the relation with: `ClassroomStudent.create(user: example_user, classroom: example_classroom)` or `example_user.classrooms << example_classroom` or `example_classroom.users << example_user`. I'm assuming the field names from your original example, so change them as appropriate if you've used the ones from my answer. – tmw Nov 29 '15 at 15:32