Association for polymorphic belongs_to of a particular type

前端 未结 5 931
小鲜肉
小鲜肉 2020-12-14 08:26

I\'m relatively new to Rails. I would like to add an association to a model that uses the polymorphic association, but returns only models of a particular type, e.g.:

<
5条回答
  •  难免孤独
    2020-12-14 08:32

    I believe I have figured out a decent way to handle this that also covers most use cases that one might need.

    I will say, it is a hard problem to find an answer to, as it is hard to figure out how to ask the question, and also hard to weed out all the articles that are just standard Rails answers. I think this problem falls into the advanced ActiveRecord realm.

    Essentially what we are trying to do is to add a relationship to the model and only use that association if certain prerequisites are met on the model where the association is made. For example, if I have class SomeModel, and it has belongs_to association called "some_association", we might want to apply some prerequisite conditions that must be true on the SomeModel record that influence whether :some_association returns a result or not. In the case of a polymorphic relationship, the prerequisite condition is that the polymorphic type column is a particular value, and if not that value, it should return nil.

    The difficulty of solving this problem is compounded by the different ways. I know of three different modes of access: direct access on an instance (ex: SomeModel.first.some_association), :joins (Ex: SomeModel.joins(:some_association), and :includes (Ex: SomeModel.includes(:some_association)) (note: eager_load is just a variation on joins). Each of these cases needs to be handled in a specific way.

    Today, as I've essentially been revisiting this problem, I came up with the following utility method that acts as a kind of wrapper method for belongs_to. I'm guessing a similar approach could be used for other association types.

      # WARNING: the joiner table must not be aliased to something else in the query,
      #  A parent / child relationship on the same table probably would not work here
      # TODO: figure out how to support a second argument scope being passed
      def self.belongs_to_with_prerequisites(name, prerequisites: {}, **options)
        base_class = self
        belongs_to name, -> (object=nil) {
          # For the following explanation, assume we have an ActiveRecord class "SomeModel" that has a belongs_to
          #   relationship on it called "some_association"
          # Object will be one of the following:
          #   * nil - when this association is loaded via an :includes.
          #     For example, SomeModel.includes(:some_association)
          #   * an model instance - when this association is called directly on the referring model
          #     For example: SomeModel.first.some_association, object will equal SomeModel.first
          #   * A JoinDependency - when we are joining this association
          #     For example, SomeModel.joins(:some_assocation)
          if !object.is_a?(base_class)
            where(base_class.table_name => prerequisites)
          elsif prerequisites.all? {|name, value| object.send(name) == value}
            self
          else
            none
          end
        },
        options
      end
    

    That method would need to be injected into ActiveRecord::Base.

    Then we could use it like:

      belongs_to_with_prerequisites :volunteer,
        prerequisites: { subject_type: 'Volunteer' },
        polymorphic: true,
        foreign_type: :subject_type,
        foreign_key: :subject_id
    

    And it would allow us to do the following:

    Note.first.volunteer
    Note.joins(:volunteer)
    Note.eager_load(:volunteer)
    

    However, we'll get an error if we try to do this:

    Note.includes(:volunteer)
    

    If we run that last bit of code, it will tell us that the column subject_type does not exist on the volunteers table.

    So we'd have to add a scope to the Notes class and use as follows:

    class Note < ActiveRecord::Base
      belongs_to_with_prerequisites :volunteer,
            prerequisites: { subject_type: 'Volunteer' },
            polymorphic: true,
            foreign_type: :subject_type,
            foreign_key: :subject_id
      scope :with_volunteer, -> { includes(:volunteer).references(:volunteer) }
    end
    Note.with_volunteer
    

    So at the end of the day, we don't have the extra join table that @stackNG's solution had, but that solution was definitely more eloquent and less hacky. Figured I'd post this anyway as it has been the result of a very thorough investigation and might help somebody else understand how this stuff works.

提交回复
热议问题