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
          records.flatten.each do |record|
            raise_on_type_mismatch(record)
            record.save! if record.new_record?
          end
        end

        super
34
      end
35

36
      def insert_record(record, validate = true)
37
        ensure_not_nested
38
        return if record.new_record? && !record.save(:validate => validate)
39

40 41 42 43
        through_record(record).save!
        update_counter(1)
        record
      end
44

45 46
      private

47
        def through_record(record)
48
          through_association = owner.association(through_reflection.name)
49 50 51 52 53 54 55 56
          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)
57
            through_record.send("#{source_reflection.name}=", record)
58 59 60 61 62 63
          end

          through_record
        end

        def build_record(attributes)
64
          ensure_not_nested
65

66 67
          record = super(attributes)

68
          inverse = source_reflection.inverse_of
69 70 71 72 73 74
          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
75
          end
76 77

          record
78 79
        end

80
        def target_reflection_has_associated_record?
81
          if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
82 83 84 85 86 87
            false
          else
            true
          end
        end

88 89 90
        def update_through_counter?(method)
          case method
          when :destroy
91
            !inverse_updates_counter_cache?(through_reflection)
92 93 94 95 96
          when :nullify
            false
          else
            true
          end
97
        end
98

99
        def delete_records(records, method)
100
          ensure_not_nested
J
Jon Leighton 已提交
101

102
          through = owner.association(through_reflection.name)
103
          scope   = through.scoped.where(construct_join_attributes(*records))
104

105 106
          case method
          when :destroy
107
            count = scope.destroy_all.length
108
          when :nullify
109
            count = scope.update_all(source_reflection.foreign_key => nil)
110
          else
111
            count = scope.delete_all
112 113
          end

114
          delete_through_records(through, records)
J
Jon Leighton 已提交
115

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

120
          update_counter(-count)
121
        end
122

123
        def delete_through_records(through, records)
124
          if through_reflection.macro == :has_many
125 126 127 128 129 130 131 132
            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
133 134
        end

135
        def find_target
136
          return [] unless target_reflection_has_associated_record?
137
          scoped.all
138
        end
139 140

        # NOTE - not sure that we can actually cope with inverses here
141
        def invertible_for?(record)
142 143
          false
        end
144 145 146
    end
  end
end