Skip to content
This repository
tag: v3.1.0.rc1
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 178 lines (163 sloc) 7.744 kb
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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
module ActiveRecord
  module Associations
    # Implements the details of eager loading of Active Record associations.
    #
    # Note that 'eager loading' and 'preloading' are actually the same thing.
    # However, there are two different eager loading strategies.
    #
    # The first one is by using table joins. This was only strategy available
    # prior to Rails 2.1. Suppose that you have an Author model with columns
    # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
    # this strategy, Active Record would try to retrieve all data for an author
    # and all of its books via a single query:
    #
    # SELECT * FROM authors
    # LEFT OUTER JOIN books ON authors.id = books.id
    # WHERE authors.name = 'Ken Akamatsu'
    #
    # However, this could result in many rows that contain redundant data. After
    # having received the first row, we already have enough data to instantiate
    # the Author object. In all subsequent rows, only the data for the joined
    # 'books' table is useful; the joined 'authors' data is just redundant, and
    # processing this redundant data takes memory and CPU time. The problem
    # quickly becomes worse and worse as the level of eager loading increases
    # (i.e. if Active Record is to eager load the associations' associations as
    # well).
    #
    # The second strategy is to use multiple database queries, one for each
    # level of association. Since Rails 2.1, this is the default strategy. In
    # situations where a table join is necessary (e.g. when the +:conditions+
    # option references an association's column), it will fallback to the table
    # join strategy.
    class Preloader #:nodoc:
      autoload :Association, 'active_record/associations/preloader/association'
      autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
      autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
      autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'

      autoload :HasMany, 'active_record/associations/preloader/has_many'
      autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
      autoload :HasOne, 'active_record/associations/preloader/has_one'
      autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
      autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
      autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'

      attr_reader :records, :associations, :options, :model

      # Eager loads the named associations for the given Active Record record(s).
      #
      # In this description, 'association name' shall refer to the name passed
      # to an association creation method. For example, a model that specifies
      # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
      # names +:author+ and +:buyers+.
      #
      # == Parameters
      # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
      # i.e. +records+ itself may also contain arrays of records. In any case,
      # +preload_associations+ will preload the all associations records by
      # flattening +records+.
      #
      # +associations+ specifies one or more associations that you want to
      # preload. It may be:
      # - a Symbol or a String which specifies a single association name. For
      # example, specifying +:books+ allows this method to preload all books
      # for an Author.
      # - an Array which specifies multiple association names. This array
      # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
      # allows this method to preload an author's avatar as well as all of his
      # books.
      # - a Hash which specifies multiple association names, as well as
      # association names for the to-be-preloaded association objects. For
      # example, specifying <tt>{ :author => :avatar }</tt> will preload a
      # book's author, as well as that author's avatar.
      #
      # +:associations+ has the same format as the +:include+ option for
      # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
      #
      # :books
      # [ :books, :author ]
      # { :author => :avatar }
      # [ :books, { :author => :avatar } ]
      #
      # +options+ contains options that will be passed to ActiveRecord::Base#find
      # (which is called under the hood for preloading records). But it is passed
      # only one level deep in the +associations+ argument, i.e. it's not passed
      # to the child associations when +associations+ is a Hash.
      def initialize(records, associations, options = {})
        @records = Array.wrap(records).compact.uniq
        @associations = Array.wrap(associations)
        @options = options
      end

      def run
        unless records.empty?
          associations.each { |association| preload(association) }
        end
      end

      private

      def preload(association)
        case association
        when Hash
          preload_hash(association)
        when String, Symbol
          preload_one(association.to_sym)
        else
          raise ArgumentError, "#{association.inspect} was not recognised for preload"
        end
      end

      def preload_hash(association)
        association.each do |parent, child|
          Preloader.new(records, parent, options).run
          Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
        end
      end

      # Not all records have the same class, so group then preload group on the reflection
      # itself so that if various subclass share the same association then we do not split
      # them unnecessarily
      #
      # Additionally, polymorphic belongs_to associations can have multiple associated
      # classes, depending on the polymorphic_type field. So we group by the classes as
      # well.
      def preload_one(association)
        grouped_records(association).each do |reflection, klasses|
          klasses.each do |klass, records|
            preloader_for(reflection).new(klass, records, reflection, options).run
          end
        end
      end

      def grouped_records(association)
        Hash[
          records_by_reflection(association).map do |reflection, records|
            [reflection, records.group_by { |record| association_klass(reflection, record) }]
          end
        ]
      end

      def records_by_reflection(association)
        records.group_by do |record|
          reflection = record.class.reflections[association]

          unless reflection
            raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
                                                    "perhaps you misspelled it?"
          end

          reflection
        end
      end

      def association_klass(reflection, record)
        if reflection.macro == :belongs_to && reflection.options[:polymorphic]
          klass = record.send(reflection.foreign_type)
          klass && klass.constantize
        else
          reflection.klass
        end
      end

      def preloader_for(reflection)
        case reflection.macro
        when :has_many
          reflection.options[:through] ? HasManyThrough : HasMany
        when :has_one
          reflection.options[:through] ? HasOneThrough : HasOne
        when :has_and_belongs_to_many
          HasAndBelongsToMany
        when :belongs_to
          BelongsTo
        end
      end
    end
  end
end
Something went wrong with that request. Please try again.