has_many_through_association.rb 4.2 KB
Newer Older
1
require 'active_support/core_ext/object/blank'
2

3
module ActiveRecord
4
  # = Active Record Has Many Through Association
5
  module Associations
6
    class HasManyThroughAssociation < HasManyAssociation #:nodoc:
7
      include ThroughAssociation
8

9
      alias_method :new, :build
10

11 12 13
      # 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
14
      # SELECT query if you use #length.
15
      def size
16
        if has_cached_counter?
17
          owner.send(:read_attribute, cached_counter_attribute_name)
18
        elsif loaded?
19
          target.size
20 21 22
        else
          count
        end
23
      end
24

25
      def concat(*records)
26
        unless owner.new_record?
27 28 29 30 31 32 33 34 35
          records.flatten.each do |record|
            raise_on_type_mismatch(record)
            record.save! if record.new_record?
          end
        end

        super
      end

36 37 38 39 40 41
      def insert_record(record, validate = true)
        return if record.new_record? && !record.save(:validate => validate)
        through_record(record).save!
        update_counter(1)
        record
      end
42

43 44
      private

45
        def through_record(record)
46
          through_association = owner.association(reflection.through_reflection.name)
47 48 49 50 51 52 53 54
          attributes = construct_join_attributes(record)

          through_record = Array.wrap(through_association.target).find { |candidate|
            candidate.attributes.slice(*attributes.keys) == attributes
          }

          unless through_record
            through_record = through_association.build(attributes)
55
            through_record.send("#{reflection.source_reflection.name}=", record)
56 57 58 59 60 61 62 63
          end

          through_record
        end

        def build_record(attributes)
          record = super(attributes)

64
          inverse = reflection.source_reflection.inverse_of
65 66 67 68 69 70 71 72 73 74 75
          if inverse
            if inverse.macro == :has_many
              record.send(inverse.name) << through_record(record)
            elsif inverse.macro == :has_one
              record.send("#{inverse.name}=", through_record(record))
            end
          end

          record
        end

76
        def target_reflection_has_associated_record?
77
          if reflection.through_reflection.macro == :belongs_to && owner[reflection.through_reflection.foreign_key].blank?
78 79 80 81 82 83
            false
          else
            true
          end
        end

84 85 86
        def update_through_counter?(method)
          case method
          when :destroy
87
            !inverse_updates_counter_cache?(reflection.through_reflection)
88 89 90 91 92
          when :nullify
            false
          else
            true
          end
93
        end
94

95
        def delete_records(records, method)
96
          through = owner.association(reflection.through_reflection.name)
97 98
          scope   = through.scoped.where(construct_join_attributes(*records))

99 100
          case method
          when :destroy
101
            count = scope.destroy_all.length
102
          when :nullify
103
            count = scope.update_all(reflection.source_reflection.foreign_key => nil)
104
          else
105
            count = scope.delete_all
106
          end
107

108 109
          delete_through_records(through, records)

110 111
          if reflection.through_reflection.macro == :has_many && update_through_counter?(method)
            update_counter(-count, reflection.through_reflection)
112 113 114
          end

          update_counter(-count)
115 116
        end

117
        def delete_through_records(through, records)
118
          if reflection.through_reflection.macro == :has_many
119 120 121 122 123 124 125 126 127 128
            records.each do |record|
              through.target.delete(through_record(record))
            end
          else
            records.each do |record|
              through.target = nil if through.target == through_record(record)
            end
          end
        end

129
        def find_target
130
          return [] unless target_reflection_has_associated_record?
131
          scoped.all
132
        end
133

134
        # NOTE - not sure that we can actually cope with inverses here
135
        def invertible_for?(record)
136 137
          false
        end
138 139 140
    end
  end
end