Andrew
Andrew

Reputation: 1013

Rails many to many polymorphic on both sides

Rails 5

Looking on how to do a join table with has many through relationships where both sides are polymorphic and the relationships are just parents or children which could be of any class

I'm building a Laboratory Information Management System and inside this system there are approximately 30 "data modules" (each their own model) which fall into three different categories and can generally be parents or children to any of the other 30 data module classes

So lets say I have DataModuleA - DataModuleZ (ie 26 modules one for each letter)

I want any of those modules to be either a parent or a child to other modules and I want to be able to call .child_modules or .parent_modules on any of those objects and get a list of all the parents or children

Right now I have a join table module_relationships

    create_table :module_relationships, id: :uuid do |t|
      t.references   :child_module, polymorphic: true, type: :uuid, index: {:name => 'index_module_relationships_on_child_type_and_id'}
      t.references   :parent_module, polymorphic: true, type: :uuid, index: {:name => 'index_module_relationships_on_parent_type_and_id'}

      t.timestamps
    end

And then an abstract base class which all of the modules can inherit from

class ApplicationModule < ApplicationRecord
  ## maybe should go back to being a concern where individual has many throughs are determined at the class level
  self.abstract_class = true

  has_many :child_module_relationships, class_name: "ModuleRelationship", foreign_key: "parent_module"
  has_many :parent_module_relationships, class_name: "ModuleRelationship", foreign_key: "child_module"

  # has_many :child_modules, through: :child_module_relationships ## doesnt work now
  # has_many :parent_modules, through: :parent_module_relationships ## doesnt work now


  def create_child_module_relationship(child_module)
    ModuleRelationship.create(
      parent_module_id: self.id,
      parent_module_type: self.class.to_s,
      child_module_id: child_module.id,
      child_module_type: child_module.class.to_s
    )
  end

  def create_parent_module_relationship(parent_module)
    ModuleRelationship.create(
      child_module_id: self.id,
      child_module_type: self.class.to_s,
      parent_module_id: parent_module.id,
      parent_module_type: parent_module.class.to_s
    )
  end
end

Right now trying to call .child_modules throws an error:

ActiveRecord::HasManyThroughAssociationPolymorphicSourceError (Cannot have a has_many :through association 'ApplicationModule#child_modules' on the polymorphic object 'ChildModule#child_module' without 'source_type'. Try adding 'source_type: "ChildModule"' to 'has_many :through' definition.)

Which more or less makes sense to me but I thought that was the whole point of defining the type on the join table so it would know where to look for the objects.

Should I just write a scope and do a nasty n+1 query for each ModuleRelationship Record?

Any other thoughts on how I could get the .child_modules and .parent_modules to return an a multi_model list of modules ?

Upvotes: 0

Views: 189

Answers (1)

matiss
matiss

Reputation: 747

Not sure if I understand your question correctly, but here is something that maybe helps you to generate some ideas for your solution.

An example where I'm storing countries and link countries to companies:

class Admin::SystemValues::Country < ApplicationRecord
  has_many :linked_countries, class_name: "Common::LinkedCountry"
  has_many :companies, class_name: "Common::Company", through: :linked_countries,
           source: :countryable, source_type: "Common::Company"
end

class Common::LinkedCountry < ApplicationRecord
# == Schema Information
#
# Table name: common_linked_countries
#
#  id               :bigint(8)        not null, primary key
#  country_id       :integer          not null
#  countryable_id   :bigint(8)        not null
#  countryable_type :string           not null
#  updated_at       :datetime
#
# Indexes
#
#  index_common_linked_countries_on_country_and_countryable  (country_id,countryable_id,countryable_type) UNIQUE
#
  belongs_to :countryable, polymorphic: true
  belongs_to :country, class_name: 'Admin::SystemValues::Country', optional: true
end

class Common::Company < ApplicationRecord
  has_many :linked_countries, class_name: 'Common::LinkedCountry', as: :countryable
  has_many :countries, class_name: 'Admin::SystemValues::Country', through: :linked_countries
end

So basically I can link Country to any model I want through country_id. I can get my countries with Common::Company.first.countries.

Maybe you can somehow store those DataModuleA - DataModuleZ (ie 26 modules one for each letter) within one model and then do something like above. For example maybe you can store an attribute like letter (a..z) and then you have all data in one model and table to query later.

Upvotes: 1

Related Questions