user2719094
user2719094

Reputation: 1752

How do I properly alias a has_many through a join in Rails ActiveRecord?

Rails/ActiveRecord newbie here. Consider the following models for a Classroom, User and ClassroomEnrollments (join between the two)

class Classroom < ActiveRecord::Base
  has_many :fulltime_enrollments, -> { where(duration: 'full-time') }, class_name: "ClassroomEnrollments"
  has_many :fulltimers, :through => :fulltime_enrollments, class_name: "User"

  has_many :parttime_enrollments, -> { where(duration: 'part-time') }, class_name: "ClassroomEnrollments"
  has_many :parttimers, :through => :parttime_enrollments, class_name: "User"
end

class ClassroomEnrollment < ActiveRecord::Base
  # columns: user_id, classroom_id, duration
  belongs_to :user
  belongs_to :classroom
end

class User < ActiveRecord::Base
  has_many :classroom_enrollments
  has_many :classrooms, :through => :classroom_enrollments
end

The following model for a classroom and classroom_enrollments does not work. Specifically the :fulltimers and :parttimers aliases throw undefined method 'to_sym' for nil:NilClass errors when I try to access them via my_classroom.fulltimers or my_classroom.parttimers.

If I remove the :parttimers alias and rename :fulltimers to :users it works fine (and displays only the full time students), so it seems to me that it has something to do with it figuring out that :fulltimers is of type User, even though I've specified the classname: "User" in the has_many condition.

What am I doing wrong?

Upvotes: 2

Views: 6783

Answers (2)

Jimbo511
Jimbo511

Reputation: 11

I stumbled on this while working on something similar. This is will generate the same sql and is a bit easier to look at.

class Classroom < ActiveRecord::Base
  has_many :classroom_enrollments
  has_many :users, through: :classroom_enrollments

  def fulltimers
    users.merge(ClassroomEnrollment.full_time)
  end

  def parttimers
    users.merge(ClassroomEnrollment.part_time)
  end
end

class ClassroomEnrollment < ActiveRecord::Base
  belongs_to :user
  belongs_to :classroom

  scope :fulltime, ->{ where(duration: 'full-time') }
  scope :parttime, ->{ where(duration: 'part-time') }
end

class User < ActiveRecord::Base
  has_many :classroom_enrollments
  has_many :classrooms, :through => :classroom_enrollments
end

Upvotes: 1

Jordan Allan
Jordan Allan

Reputation: 4486

Since the source association cannot be inferred automatically, you need specify it using the :source option:

class Classroom < ActiveRecord::Base
  has_many(
    :fulltime_enrollments, 
    -> { where(duration: 'full-time') }, 
    class_name: "ClassroomEnrollments"
  )
  has_many :fulltimers, :through => :fulltime_enrollments, :source => :user

  has_many(
    :parttime_enrollments, 
    -> { where(duration: 'part-time') }, 
    class_name: "ClassroomEnrollments"
  )
  has_many :parttimers, :through => :parttime_enrollments, :source => :user
end

http://guides.rubyonrails.org/association_basics.html#options-for-has-many-source

How about trying a cleaner, more readable approach? Something like this:

class Classroom < ActiveRecord::Base
  has_many :classroom_enrollments
  has_many :users, through: :classroom_enrollments

  def full_timers
    users_by_duration("full-time")
  end

  def part_timers
    users_by_duration("part-time")
  end

  private

  def users_by_duration(duration)
    users.where(classroom_enrollments: { duration: duration })
  end
end

Then:

my_classroom = Classroom.find(1)
my_classroom.full_timers

Upvotes: 10

Related Questions