efigence/has_reindexable_associations

View on GitHub
lib/has_reindexable_associations.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'has_reindexable_associations/version'
require 'rails/all'
require 'searchkick'

# To avoid full model reindex, one can't just call reindex on a scope.
# One must call reindex_async on each associated record.
# Usage:
# MyClass < ActiveRecord::Base
# include HasReindexableAssociations
# belongs_to :some_association
# has_many :some_associations
# has_reindexable_associations :some_association, :some_associations
module HasReindexableAssociations
  extend ActiveSupport::Concern

  class_methods do
    cattr_writer :reindexable_associations

    # Set this to `true` before any seeds or imports to reindex associations later on your own like this:
    # MyClass.reindexable_associations_skip = true
    # MyClass.import(...)
    # MyClass.reindex
    # MyClass.reindexable_associations_skip = false
    # MyClass.reindexable_associations.each { |association| MyClass.send(association).model.reindex }
    cattr_accessor :reindexable_associations_skip

    def has_reindexable_associations(*args)
      # @@ is shared between classes via concern, not what we want
      # @@reindexable_associations = args
      class_variable_set :@@reindexable_associations, args
    end

    def reindexable_associations
      # @@reindexable_associations || []
      class_variable_get :@@reindexable_associations
    rescue NameError
      []
    end
  end

  included do
    after_commit :reindex_associations

    private

    def reindex_associations
      self.class.reindexable_associations.each do |reindexable_association|
        reindex_associations_process(reindexable_association)
      end
    end

    def reindex_associations_process(reindexable_association)
      record_or_records = reindex_associations_scope(reindexable_association)
      if record_or_records.respond_to?(:to_a)
        reindex_associations_process_records(record_or_records, reindexable_association)
      else
        reindex_associations_process_record(record_or_records, reindexable_association)
      end
    end

    def reindex_associations_scope(reindexable_association)
      send(reindexable_association) # record_or_records
    rescue NoMethodError
      raise NoMethodError.new("foreign key '#{reindexable_association}' is not defined, verify has_reindexable_associations options")
    end

    def reindex_associations_process_records(records, reindexable_association)
      unless self.class.reindexable_associations_skip
        records.to_a.each do |record|
          reindex_associations_process_record(record, reindexable_association)
        end
      end
    end

    def reindex_associations_process_record(record, reindexable_association)
      reindex_association(record)
      reindex_associations_process_old_record(reindexable_association)
    end

    def reindex_association(record)
      record.reindex_async
    end

    def reindex_associations_find_old_polymorphic_association(reindexable_association)
      key, type = reindex_associations_describe_polymorphic_association(reindexable_association)
      old_klass = reindex_associations_describe_old_polymorphic_association(key, type)
      return unless old_klass && previous_changes[key] && previous_changes[key].first
      old_klass.where(id: previous_changes[key].first).first # old_record
    end

    def reindex_associations_process_old_record(reindexable_association)
      old_record = reindex_associations_find_old_polymorphic_association(reindexable_association)
      reindex_association(old_record) if old_record
    end

    def reindex_associations_describe_polymorphic_association(reindexable_association)
      key = self.class.reflect_on_association(reindexable_association).try(:foreign_key)
      type = self.class.reflect_on_association(reindexable_association).try(:foreign_type)
      [key, type]
    end

    def reindex_associations_describe_old_polymorphic_association(key, type)
      if (previous_changes[key] && previous_changes[key].first) || (previous_changes[type] && previous_changes[type].first)
        old_klass = previous_changes[type].first rescue NameError
        send(type).constantize if old_klass == NameError # old_klass
      end
    rescue NameError
    end
  end
end