lib/json_api/controllers/concerns/resource_actions/include_preloading.rb
# frozen_string_literal: true module JSONAPI module ResourceActions module IncludePreloading extend ActiveSupport::Concern private def includes_to_hash(paths) hash = paths.each_with_object({}) do |path, h| path.split(".").reduce(h) { |cur, part| cur[part.to_sym] ||= {} } end filter_includable(hash, model_class) end # Preloads associations from the include param so the serializer avoids N+1 queries # when walking include paths (association.loaded? is true). Used by both show and index. def scope_with_includes(scope) includes = parse_include_param return scope unless includes.any? inc_hash = includes_to_hash(includes) hash_contains_polymorphic?(inc_hash, model_class) ? scope.preload(inc_hash) : scope.includes(inc_hash) end def filter_includable(hash, klass) hash.each_with_object({}) do |(key, value), filtered| assoc = klass.reflect_on_association(key) next unless assoc filtered[key] = value.empty? || assoc.polymorphic? ? value : filter_includable(value, assoc.klass) end end def hash_contains_polymorphic?(hash, klass) hash.any? { |key, value| polymorphic_in_hash_entry?(key, value, klass) } end def polymorphic_in_hash_entry?(key, value, klass) assoc = klass.reflect_on_association(key) return false unless assoc assoc.polymorphic? || (value.present? && hash_contains_polymorphic?(value, assoc.klass)) end def preload_included_resource_associations(resources, includes) return if includes.empty? || resources.empty? includes.each do |include_path| association_name = include_path.split(".").first.to_sym assoc_reflection = model_class.reflect_on_association(association_name) next unless assoc_reflection targets = collect_include_targets(resources, association_name) next if targets.empty? apply_preloads_for_targets(targets, assoc_reflection.polymorphic?) end end def apply_preloads_for_targets(targets, polymorphic) if polymorphic targets.group_by(&:class).each_value { |recs| apply_resource_preloads(recs) } else apply_resource_preloads(targets) end end def collect_include_targets(resources, association_name) resources.flat_map do |r| target = r.association(association_name).target target.respond_to?(:to_a) ? target.to_a : Array(target) end.compact.uniq end def apply_resource_preloads(records) return if records.empty? klass = records.first.class resource_scope = ResourceLoader.find_for_model(klass).records preload_values = resource_scope.preload_values + resource_scope.includes_values return if preload_values.empty? ActiveRecord::Associations::Preloader.new(records:, associations: preload_values).call end end end end