association_collection.rb 7.3 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
      end
10

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
        result = true
20
        load_target if @owner.new_record?
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
      # Calculate sum using SQL, not Enumerable
46 47
      def sum(*args, &block)
        calculate(:sum, *args, &block)
48 49
      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 it hasn't happened already
70

71
        if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
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
      
      def create(attrs = {})
89 90 91 92 93
        if attrs.is_a?(Array)
          attrs.collect { |attr| create(attr) }
        else
          create_record(attrs) { |record| record.save }
        end
94 95
      end

96
      def create!(attrs = {})
97
        create_record(attrs) { |record| record.save! }
98 99
      end

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

114 115 116
      # 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
117
        load_target.size
118
      end
119

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

124
      def any?(&block)
125
        if block_given?
126
          method_missing(:any?, &block)
127 128 129
        else
          !empty?
        end
130
      end
131

D
Initial  
David Heinemeier Hansson 已提交
132
      def uniq(collection = self)
133 134 135 136 137 138 139 140
        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 已提交
141 142
      end

143 144
      # Replace this collection with +other_array+
      # This will perform a diff and delete/add only records that have changed.
145
      def replace(other_array)
146 147 148 149 150
        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
151

152 153 154 155
        @owner.transaction do
          delete(@target.select { |v| !other.include?(v) })
          concat(other_array.select { |v| !current.include?(v) })
        end
156
      end
D
Initial  
David Heinemeier Hansson 已提交
157

158

159
      protected
160
        def method_missing(method, *args, &block)
161
          if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
162
            super
163
          else
164
            @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
165 166 167 168 169 170 171 172
          end
        end

        # overloaded in derived Association classes to provide useful scoping depending on association type.
        def construct_scope
          {}
        end

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
        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 已提交
188
      private
189

190
        def create_record(attrs, &block)
191 192
          ensure_owner_is_not_new
          record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
193
          add_record_to_target_with_callbacks(record, &block)
194 195
        end

196
        def build_record(attrs, &block)
197
          record = @reflection.klass.new(attrs)
198
          add_record_to_target_with_callbacks(record, &block)
199 200 201 202 203 204 205 206 207 208 209
        end

        def add_record_to_target_with_callbacks(record)
          callback(:before_add, record)
          yield(record) if block_given?
          @target ||= [] unless loaded?
          @target << record
          callback(:after_add, record)
          record
        end

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
        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)
228 229
          full_callback_name = "#{callback_name}_for_#{@reflection.name}"
          @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
230 231 232 233 234 235 236 237
        end   
        
        def ensure_owner_is_not_new
          if @owner.new_record?
            raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
          end
        end
               
D
Initial  
David Heinemeier Hansson 已提交
238 239
    end
  end
240
end