aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/preloader/through_association.rb
blob: b16fca7dc9494be8463496e253a75544c82d959d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# frozen_string_literal: true

module ActiveRecord
  module Associations
    class Preloader
      module ThroughAssociation #:nodoc:
        def through_reflection
          reflection.through_reflection
        end

        def source_reflection
          reflection.source_reflection
        end

        def associated_records_by_owner(preloader)
          already_loaded = owners.first.association(through_reflection.name).loaded?
          through_scope = through_scope()

          unless already_loaded
            preloader.preload(owners, through_reflection.name, through_scope)
          end

          through_records = owners.map do |owner|
            center = owner.association(through_reflection.name).target
            [owner, Array(center)]
          end

          if already_loaded
            if source_type = reflection.options[:source_type]
              through_records.map! do |owner, center|
                center = center.select do |record|
                  record[reflection.foreign_type] == source_type
                end
                [owner, center]
              end
            end
          else
            reset_association(owners, through_reflection.name, through_scope)
          end

          middle_records = through_records.flat_map(&:last)

          if preload_scope
            reflection_scope = reflection_scope().merge(preload_scope)
          elsif reflection.scope
            reflection_scope = reflection_scope()
          end

          preloaders = preloader.preload(middle_records,
                                         source_reflection.name,
                                         reflection_scope)

          @preloaded_records = preloaders.flat_map(&:preloaded_records)

          middle_to_pl = preloaders.each_with_object({}) do |pl, h|
            pl.owners.each { |middle|
              h[middle] = pl
            }
          end

          through_records.each_with_object({}) do |(lhs, center), records_by_owner|
            pl_to_middle = center.group_by { |record| middle_to_pl[record] }

            records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
              rhs_records = middles.flat_map { |r|
                r.association(source_reflection.name).target
              }.compact

              # Respect the order on `reflection_scope` if it exists, else use the natural order.
              if reflection_scope && !reflection_scope.order_values.empty?
                @id_map ||= id_to_index_map @preloaded_records
                rhs_records.sort_by { |rhs| @id_map[rhs] }
              else
                rhs_records
              end
            end
          end.tap do
            reset_association(middle_records, source_reflection.name, preload_scope)
          end
        end

        private

          def id_to_index_map(ids)
            id_map = {}
            ids.each_with_index { |id, index| id_map[id] = index }
            id_map
          end

          def reset_association(owners, association_name, should_reset)
            # Don't cache the association - we would only be caching a subset
            if should_reset
              owners.each { |owner|
                owner.association(association_name).reset
              }
            end
          end

          def through_scope
            scope = through_reflection.klass.unscoped
            options = reflection.options

            if options[:source_type]
              scope.where! reflection.foreign_type => options[:source_type]
            elsif !reflection_scope.where_clause.empty?
              scope.where_clause = reflection_scope.where_clause
              values = reflection_scope.values

              if includes = values[:includes]
                scope.includes!(source_reflection.name => includes)
              else
                scope.includes!(source_reflection.name)
              end

              if values[:references] && !values[:references].empty?
                scope.references!(values[:references])
              else
                scope.references!(source_reflection.table_name)
              end

              if joins = values[:joins]
                scope.joins!(source_reflection.name => joins)
              end

              if left_outer_joins = values[:left_outer_joins]
                scope.left_outer_joins!(source_reflection.name => left_outer_joins)
              end

              if scope.eager_loading? && order_values = values[:order]
                scope = scope.order(order_values)
              end
            end

            scope unless scope.empty_scope?
          end
      end
    end
  end
end