association.rb 3.8 KB
Newer Older
J
Jon Leighton 已提交
1 2 3 4
module ActiveRecord
  module Associations
    class Preloader
      class Association #:nodoc:
5 6 7 8 9 10 11 12
        attr_reader :owners, :reflection, :preload_scope, :model, :klass

        def initialize(klass, owners, reflection, preload_scope)
          @klass         = klass
          @owners        = owners
          @reflection    = reflection
          @preload_scope = preload_scope
          @model         = owners.first && owners.first.class
J
Jon Leighton 已提交
13
          @scope         = nil
14
          @owners_by_key = nil
J
Jon Leighton 已提交
15 16 17 18 19 20 21 22 23 24 25 26
        end

        def run
          unless owners.first.association(reflection.name).loaded?
            preload
          end
        end

        def preload
          raise NotImplementedError
        end

J
Jon Leighton 已提交
27 28
        def scope
          @scope ||= build_scope
J
Jon Leighton 已提交
29 30 31
        end

        def records_for(ids)
A
Aaron Patterson 已提交
32
          query_scope(ids)
A
Aaron Patterson 已提交
33 34 35
        end

        def query_scope(ids)
J
Jon Leighton 已提交
36
          scope.where(association_key.in(ids))
J
Jon Leighton 已提交
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
        end

        def table
          klass.arel_table
        end

        # The name of the key on the associated records
        def association_key_name
          raise NotImplementedError
        end

        # This is overridden by HABTM as the condition should be on the foreign_key column in
        # the join table
        def association_key
          table[association_key_name]
        end

        # The name of the key on the model which declares the association
        def owner_key_name
          raise NotImplementedError
        end

        # We're converting to a string here because postgres will return the aliased association
        # key in a habtm as a string (for whatever reason)
        def owners_by_key
          @owners_by_key ||= owners.group_by do |owner|
            key = owner[owner_key_name]
            key && key.to_s
          end
        end

        def options
          reflection.options
        end

        private

        def associated_records_by_owner
75 76
          owners_map = owners_by_key
          owner_keys = owners_map.keys.compact
J
Jon Leighton 已提交
77

78 79 80 81
          # Each record may have multiple owners, and vice-versa
          records_by_owner = Hash[owners.map { |owner| [owner, []] }]

          if klass && owner_keys.any?
J
Jon Leighton 已提交
82 83
            # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
            # Make several smaller queries if necessary or make one query if the adapter supports it
84
            sliced  = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
A
Aaron Patterson 已提交
85 86 87 88 89 90 91 92
            sliced.each { |slice|
              records = records_for(slice)
              records.each do |record|
                owner_key = owner_id_for records, record

                owners_map[owner_key].each do |owner|
                  records_by_owner[owner] << record
                end
93
              end
A
Aaron Patterson 已提交
94
            }
J
Jon Leighton 已提交
95
          end
96

J
Jon Leighton 已提交
97 98 99
          records_by_owner
        end

100 101 102 103
        def owner_id_for(results, record)
          record[association_key_name].to_s
        end

104
        def reflection_scope
105
          @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
106 107
        end

J
Jon Leighton 已提交
108
        def build_scope
109
          scope = klass.unscoped
J
Jon Leighton 已提交
110

111 112
          values         = reflection_scope.values
          preload_values = preload_scope.values
J
Jon Leighton 已提交
113

114 115 116 117 118
          scope.where_values      = Array(values[:where])      + Array(preload_values[:where])
          scope.references_values = Array(values[:references]) + Array(preload_values[:references])

          scope.select!   preload_values[:select] || values[:select] || table[Arel.star]
          scope.includes! preload_values[:includes] || values[:includes]
J
Jon Leighton 已提交
119 120

          if options[:as]
121
            scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
J
Jon Leighton 已提交
122 123
          end

124
          klass.default_scoped.merge(scope)
J
Jon Leighton 已提交
125 126 127 128 129
        end
      end
    end
  end
end