Search code examples
ruby-on-railsactiverecordassociationshas-many-throughsti

Rails 5: STI With Has Many Through Association


I have searched extensively for a solution to my situation, but I can't find anything.

In my application I have a Person model that only stores data about people:

class Person < ApplicationRecord
end

Then I have a Trial model. Trials can have many people using a has-many-through association. Additionally, in the context of a Trial, a person can be a Defendant or a Plaintiff. To achieve this, I set up my models like this:

class Trial < ApplicationRecord
  has_many :trial_people
  has_many :plaintiffs, class_name: 'Plaintiff', through: :trial_people, source: :person
  has_many :defendants, class_name: 'Defendant', through: :trial_people, source: :person
end

class TrialPerson < ApplicationRecord
  belongs_to :trial
  belongs_to :person
end

class Plaintiff < Person
end

class Defendant < Person
end

I am then using Select2 JQuery plugin to add in the defendants and plaintiffs for each trial in the view. Obtaining the IDs in strong parameters:

params.require(:trial).permit(:title, :description, :start_date, :plaintiff_ids => [], :defendant_ids => [])

So that I can do achieve like the following:

trial.defendants
trial.plaintiffs

The problem is that I do not have any way of distinguishing between those classes inside the trial_people table. I was thinking on adding a type column to that table (STI), but I do not know how to automatically add that type to each defendant or plaintiff when saving the Trial object.

Would appreciate some insight on how to achieve this, using STI or not.


Solution

  • One way you can go about this without changing your associations or schema is to use a before_create callback.

    Assuming you've added a person_type string column to trial_people

    class TrialPerson < ApplicationRecord
      belongs_to :trial
      belongs_to :person
      before_create :set_person_type
    
      private
    
      def set_person_type
        self.person_type = person.type
      end
    end
    

    Another way to approach it is to remove the person association and replace it with a polymorphic triable association. This achieves the same end result but it's built into the ActiveRecord API and so doesn't require any callbacks or extra custom logic.

    # migration
    
    class AddTriableReferenceToTrialPeople < ActiveRecord::Migration
      def up
        remove_reference :trial_people, :person, index: true
        add_reference :trial_people, :triable, polymorphic: true
      end
      
      def down
        add_reference :trial_people, :person, index: true
        remove_reference :trial_people, :triable, polymorphic: true
      end
    end
    
    # models
    
    class TrialPerson < ApplicationRecord
      belongs_to :trial
      belongs_to :triable, polymorphic: true
    end
    
    class Person < ApplicationRecord
      abstract_class = true
      has_many :trial_people, as: :triable
    end
    
    class Trial < ApplicationRecord
      has_many :trial_people
      has_many :defendants, source: :triable, source_type: 'Defendant', through: :trial_people
      has_many :plaintiffs, source: :triable, source_type: 'Plaintiff', through: :trial_people
    end
    
    class Plaintiff < Person
    end
    
    class Defendant < Person
    end
    

    This gives you triable_type and triable_id columns on your trial_people table which are set automatically when you add to the collections

    trial = Trial.create
    trial.defendants << Defendant.first
    trial.trial_people.first # => #<TrialPerson id: 1, trial_id: 1, triable_type: "Defendant", triable_id: 1, ... >
    

    Note that Person needs to be an abstract class as otherwise the triable_type will be set as "Person" for all Person objects including the Plaintiff and Defendant unless overwritten manually.