Home > other >  Set up many to many associations between classes in single inheritance table and another table
Set up many to many associations between classes in single inheritance table and another table

Time:05-26

I have a join table named Relations in a many to many relationship between departments and researchers.

I want to be able to get a list of students by doing Department.find(1).students but I am getting ActiveRecord::HasManyThroughSourceAssociationNotFoundError (Could not find the source association(s) :students in model Researcher. Try 'has_many :students, :through => :researchers, :source => <name>'.)

Why isn't it using the scope from the table Researcher?

class Department < ApplicationRecord
  has_many :relations
  has_many :researchers, through: :relations
  has_many :students, source: :students, through: :researchers
  has_many :advisors, source: :advisors, through: :researchers
end

class Relation < ApplicationRecord
  belongs_to :researcher
  belongs_to :department
end

class Reseacher < ApplicationRecord
  scope :students, -> { where(type: 'Student') }
  scope :advisors, -> { where(type: 'Advisor') }
end

class Student < Researcher
  has_many :relations, foreign_key: :department_id
  has_many :departments, through: :relations
end

class Advisor < Researcher
  has_many :relations, foreign_key: :department_id
  has_many :departments, through: :relations
end

CodePudding user response:

source: option expects an association as argument. Internally, rails runs a reflection on the argument, like:

# source: :students, through: :researchers
>> Researcher.reflect_on_association(:students)
=> nil

Before fixing has_many :students association, a few things to note:

has_many :students,     # will look for `students` association in the related
                        # class unless source is specified; related class is
                        # determined by reflecting on through option `:researchers`
                        #
                        #   reflect_on_association(:researchers).klass # => Researcher

  through: :researchers # can't go through `researchers`; already there.
                        # `Student` is a `Researcher`.

  source: :students,    # there is no `students` association in `Researcher` class.
                        #
                        #   reflect_on_association(:researchers).klass
                        #     .reflect_on_association(:students) # => nil

To fix the association we can use scope argument of has_many method:

has_many(name, scope = nil, **options, &extension)
#              ^ pass a proc as a second argument
class Department < ApplicationRecord
  # NOTE: add `dependent: :destroy` option to destroy corresponding Relations
  #       when destroying a Department 
  has_many :relations, dependent: :destroy
  has_many :researchers, through: :relations

  has_many :students, 
    -> { where(type: "Student") }, # scope the associated model

    through: :relations,           # relevant association is in Relation model

    source:  :researcher           # look for `researcher` association in Relation.
                                   # instead of `student`

  # NOTE: use existing scope from another model
  has_many :advisors,
    -> { advisors },               # this runs in the source class.
    through: :relations,           #                    |
    source:  :researcher           # <------------------'
                                   # Researcher has `advisors` class method,
                                   # defined by `scope: :advisors`.
end

Now, we need to fix the association between Relation and Researcher:

# NOTE: what if you need another "relation" class to make another many-to-many association.
# TODO: call this something a bit more descriptive like `DepartmentStaff`
#       or use the conventional `DepartmentResearcher`.
class Relation < ApplicationRecord
  belongs_to :researcher
  belongs_to :department
end

class Researcher < ApplicationRecord
  scope :students, -> { where(type: "Student") }
  scope :advisors, -> { where(type: "Advisor") }

  # NOTE: `has_many :relations` is the opposite of `belongs_to :researcher`
  #       `foreign_key` is `researcher_id` which is the default and
  #       should not be changed.
  # has_many :relations, foreign_key: :department_id

  has_many :relations, dependent: :destroy        # <--.
  has_many :departments, through: :relations      # <--|
end                                               #    |
                                                  #    |
class Student < Researcher                        #    |
  # NOTE: no need to duplicate these; put it in the parent class.
  # has_many :relations
  # has_many :departments, through: :relations
end

class Advisor < Researcher
end
>> Relation.create!([{researcher: Student.new, department: Department.create},{researcher: Advisor.new, department: Department.first}])

>> Department.first.students
=> [#<Student:0x00007f7f78ae5f98 id: 1, type: "Student">]

>> Department.first.advisors        
=> [#<Advisor:0x00007f7f789a9b20 id: 2, type: "Advisor">]

>> Department.first.researchers                                                            
=> [#<Student:0x00007f7f786bdc90 id: 1, type: "Student">, #<Advisor:0x00007f7f786bd858 id: 2, type: "Advisor">]

You can also let rails do the work by defining additional associations in Relation, no scope required:

class Relation < ApplicationRecord
  belongs_to :researcher
  belongs_to :department

  belongs_to :student, foreign_key: :researcher_id
  belongs_to :advisor, foreign_key: :researcher_id
end

class Department < ApplicationRecord
  has_many :relations, dependent: :destroy
  has_many :researchers, through: :relations

  has_many :students, through: :relations
  has_many :advisors, through: :relations
end

https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many

https://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html#method-i-reflect_on_association

  • Related