association_collection.rb 5.8 KB
Newer Older
1 2
require 'set'

D
Initial  
David Heinemeier Hansson 已提交
3 4
module ActiveRecord
  module Associations
5
    class AssociationCollection < AssociationProxy #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
6
      def to_ary
7 8
        load_target
        @target.to_ary
D
Initial  
David Heinemeier Hansson 已提交
9 10
      end
  
11
      def reset
12
        reset_target!
13
        @loaded = false
D
Initial  
David Heinemeier Hansson 已提交
14 15 16 17 18
      end

      # Add +records+ to this association.  Returns +self+ so method calls may be chained.  
      # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
      def <<(*records)
19 20
        result = true
        load_target
21

22 23 24
        @owner.transaction do
          flatten_deeper(records).each do |record|
            raise_on_type_mismatch(record)
25
            callback(:before_add, record)
26
            result &&= insert_record(record) unless @owner.new_record?
27
            @target << record
28
            callback(:after_add, record)
29
          end
D
Initial  
David Heinemeier Hansson 已提交
30
        end
31

32
        result && self
D
Initial  
David Heinemeier Hansson 已提交
33 34 35 36
      end

      alias_method :push, :<<
      alias_method :concat, :<<
37 38 39
                      
      # Remove all records from this association
      def delete_all
40
        load_target
41
        delete(@target)
42
        reset_target!
43
      end
D
Initial  
David Heinemeier Hansson 已提交
44

45 46 47 48 49
      # Calculate sum using SQL, not Enumerable
      def sum(*args, &block)
        calculate(:sum, *args, &block)
      end

D
Initial  
David Heinemeier Hansson 已提交
50 51 52
      # Remove +records+ from this association.  Does not destroy +records+.
      def delete(*records)
        records = flatten_deeper(records)
53
        records.each { |record| raise_on_type_mismatch(record) }
54
        records.reject! { |record| @target.delete(record) if record.new_record? }
55
        return if records.empty?
56 57
        
        @owner.transaction do
58
          records.each { |record| callback(:before_remove, record) }
59
          delete_records(records)
60 61 62 63
          records.each do |record|
            @target.delete(record)
            callback(:after_remove, record)
          end
64
        end
D
Initial  
David Heinemeier Hansson 已提交
65
      end
66 67 68

      # Removes all records from this association.  Returns +self+ so method calls may be chained.
      def clear
69
        return self if length.zero? # forces load_target if hasn't happened already
70

71
        if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
72 73 74 75
          destroy_all
        else          
          delete_all
        end
76

77 78
        self
      end
D
Initial  
David Heinemeier Hansson 已提交
79 80
      
      def destroy_all
81 82 83 84
        @owner.transaction do
          each { |record| record.destroy }
        end

85
        reset_target!
D
Initial  
David Heinemeier Hansson 已提交
86
      end
87

88 89
      def create(attributes = {})
        # Can't use Base.create since the foreign key may be a protected attribute.
90 91 92 93
        if attributes.is_a?(Array)
          attributes.collect { |attr| create(attr) }
        else
          record = build(attributes)
94
          record.save unless @owner.new_record?
95 96
          record
        end
97 98
      end

99 100 101
      # 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 less SELECT query if you use length.
D
Initial  
David Heinemeier Hansson 已提交
102
      def size
103 104 105
        if loaded? && !@reflection.options[:uniq]
          @target.size
        elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
106
          unsaved_records = Array(@target.detect { |r| r.new_record? })
D
David Heinemeier Hansson 已提交
107
          unsaved_records.size + count_records
108 109 110
        else
          count_records
        end
D
Initial  
David Heinemeier Hansson 已提交
111
      end
112

113 114 115
      # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
      # whether the collection is empty, use collection.length.zero? instead of collection.empty?
      def length
116
        load_target.size
117
      end
118

D
Initial  
David Heinemeier Hansson 已提交
119
      def empty?
120
        size.zero?
D
Initial  
David Heinemeier Hansson 已提交
121
      end
122

D
Initial  
David Heinemeier Hansson 已提交
123
      def uniq(collection = self)
124 125 126 127 128 129 130 131
        seen = Set.new
        collection.inject([]) do |kept, record|
          unless seen.include?(record.id)
            kept << record
            seen << record.id
          end
          kept
        end
D
Initial  
David Heinemeier Hansson 已提交
132 133
      end

134 135
      # Replace this collection with +other_array+
      # This will perform a diff and delete/add only records that have changed.
136
      def replace(other_array)
137 138 139 140 141
        other_array.each { |val| raise_on_type_mismatch(val) }

        load_target
        other   = other_array.size < 100 ? other_array : other_array.to_set
        current = @target.size < 100 ? @target : @target.to_set
142

143 144 145 146
        @owner.transaction do
          delete(@target.select { |v| !other.include?(v) })
          concat(other_array.select { |v| !current.include?(v) })
        end
147
      end
D
Initial  
David Heinemeier Hansson 已提交
148

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
      protected
        def reset_target!
          @target = Array.new
        end

        def find_target
          records =
            if @reflection.options[:finder_sql]
              @reflection.klass.find_by_sql(@finder_sql)
            else
              find(:all)
            end

          @reflection.options[:uniq] ? uniq(records) : records
        end

D
Initial  
David Heinemeier Hansson 已提交
165
      private
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
        def callback(method, record)
          callbacks_for(method).each do |callback|
            case callback
              when Symbol
                @owner.send(callback, record)
              when Proc, Method
                callback.call(@owner, record)
              else
                if callback.respond_to?(method)
                  callback.send(method, @owner, record)
                else
                  raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
                end
            end
          end
        end
        
        def callbacks_for(callback_name)
184 185
          full_callback_name = "#{callback_name}_for_#{@reflection.name}"
          @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
186 187
        end
        
D
Initial  
David Heinemeier Hansson 已提交
188 189
    end
  end
190
end