Skip to content
This repository
branch: master
file 215 lines (172 sloc) 6.433 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214

module ActiveRecord
  # = Active Record Has Many Through Association
  module Associations
    class HasManyThroughAssociation < HasManyAssociation #:nodoc:
      include ThroughAssociation

      def initialize(owner, reflection)
        super

        @through_records = {}
        @through_association = nil
      end

      # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
      # loaded and calling collection.size if it has. If it's more likely than not that the collection does
      # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
      # SELECT query if you use #length.
      def size
        if has_cached_counter?
          owner.read_attribute cached_counter_attribute_name(reflection)
        elsif loaded?
          target.size
        else
          count
        end
      end

      def concat(*records)
        unless owner.new_record?
          records.flatten.each do |record|
            raise_on_type_mismatch!(record)
          end
        end

        super
      end

      def concat_records(records)
        ensure_not_nested

        records = super(records, true)

        if owner.new_record? && records
          records.flatten.each do |record|
            build_through_record(record)
          end
        end

        records
      end

      def insert_record(record, validate = true, raise = false)
        ensure_not_nested

        if record.new_record?
          if raise
            record.save!(:validate => validate)
          else
            return unless record.save(:validate => validate)
          end
        end

        save_through_record(record)
        update_counter(1)
        record
      end

      private

        def through_association
          @through_association ||= owner.association(through_reflection.name)
        end

        # We temporarily cache through record that has been build, because if we build a
        # through record in build_record and then subsequently call insert_record, then we
        # want to use the exact same object.
        #
        # However, after insert_record has been called, we clear the cache entry because
        # we want it to be possible to have multiple instances of the same record in an
        # association
        def build_through_record(record)
          @through_records[record.object_id] ||= begin
            ensure_mutable

            through_record = through_association.build through_scope_attributes
            through_record.send("#{source_reflection.name}=", record)
            through_record
          end
        end

        def through_scope_attributes
          scope.where_values_hash(through_association.reflection.name.to_s)
        end

        def save_through_record(record)
          build_through_record(record).save!
        ensure
          @through_records.delete(record.object_id)
        end

        def build_record(attributes)
          ensure_not_nested

          record = super(attributes)

          inverse = source_reflection.inverse_of
          if inverse
            if inverse.macro == :has_many
              record.send(inverse.name) << build_through_record(record)
            elsif inverse.macro == :has_one
              record.send("#{inverse.name}=", build_through_record(record))
            end
          end

          record
        end

        def target_reflection_has_associated_record?
          !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
        end

        def update_through_counter?(method)
          case method
          when :destroy
            !inverse_updates_counter_cache?(through_reflection)
          when :nullify
            false
          else
            true
          end
        end

        def delete_records(records, method)
          ensure_not_nested

          # This is unoptimised; it will load all the target records
          # even when we just want to delete everything.
          records = load_target if records == :all

          scope = through_association.scope
          scope.where! construct_join_attributes(*records)

          case method
          when :destroy
            if scope.klass.primary_key
              count = scope.destroy_all.length
            else
              scope.to_a.each do |record|
                record.run_callbacks :destroy
              end

              arel = scope.arel

              stmt = Arel::DeleteManager.new arel.engine
              stmt.from scope.klass.arel_table
              stmt.wheres = arel.constraints

              count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
            end
          when :nullify
            count = scope.update_all(source_reflection.foreign_key => nil)
          else
            count = scope.delete_all
          end

          delete_through_records(records)

          if source_reflection.options[:counter_cache] && method != :destroy
            counter = source_reflection.counter_cache_column
            klass.decrement_counter counter, records.map(&:id)
          end

          if through_reflection.macro == :has_many && update_through_counter?(method)
            update_counter(-count, through_reflection)
          end

          update_counter(-count)
        end

        def through_records_for(record)
          attributes = construct_join_attributes(record)
          candidates = Array.wrap(through_association.target)
          candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
        end

        def delete_through_records(records)
          records.each do |record|
            through_records = through_records_for(record)

            if through_reflection.macro == :has_many
              through_records.each { |r| through_association.target.delete(r) }
            else
              if through_records.include?(through_association.target)
                through_association.target = nil
              end
            end

            @through_records.delete(record.object_id)
          end
        end

        def find_target
          return [] unless target_reflection_has_associated_record?
          scope.to_a
        end

        # NOTE - not sure that we can actually cope with inverses here
        def invertible_for?(record)
          false
        end
    end
  end
end
Something went wrong with that request. Please try again.