Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/tags/rel_2-0-2/activerecord/lib/active_record/associations/association_collection.rb

Revision 8049, 7.3 kB (checked in by nzkoz, 1 year ago)

Make sure that << works on has_many associations on unsaved records. Closes #9989 [hasmanyjosh]

Line 
1 require 'set'
2
3 module ActiveRecord
4   module Associations
5     class AssociationCollection < AssociationProxy #:nodoc:
6       def to_ary
7         load_target
8         @target.to_ary
9       end
10
11       def reset
12         reset_target!
13         @loaded = false
14       end
15
16       # Add +records+ to this association.  Returns +self+ so method calls may be chained. 
17       # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
18       def <<(*records)
19         result = true
20         load_target if @owner.new_record?
21
22         @owner.transaction do
23           flatten_deeper(records).each do |record|
24             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
30         end
31
32         result && self
33       end
34
35       alias_method :push, :<<
36       alias_method :concat, :<<
37
38       # Remove all records from this association
39       def delete_all
40         load_target
41         delete(@target)
42         reset_target!
43       end
44
45       # Calculate sum using SQL, not Enumerable
46       def sum(*args, &block)
47         calculate(:sum, *args, &block)
48       end
49
50       # Remove +records+ from this association.  Does not destroy +records+.
51       def delete(*records)
52         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           records.each do |record|
61             @target.delete(record)
62             callback(:after_remove, record)
63           end
64         end
65       end
66
67       # Removes all records from this association.  Returns +self+ so method calls may be chained.
68       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           destroy_all
73         else         
74           delete_all
75         end
76
77         self
78       end
79      
80       def destroy_all
81         @owner.transaction do
82           each { |record| record.destroy }
83         end
84
85         reset_target!
86       end
87      
88       def create(attrs = {})
89         if attrs.is_a?(Array)
90           attrs.collect { |attr| create(attr) }
91         else
92           create_record(attrs) { |record| record.save }
93         end
94       end
95
96       def create!(attrs = {})
97         create_record(attrs) { |record| record.save! }
98       end
99
100       # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
101       # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
102       # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
103       def size
104         if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
105           @target.size
106         elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
107           unsaved_records = Array(@target.detect { |r| r.new_record? })
108           unsaved_records.size + count_records
109         else
110           count_records
111         end
112       end
113
114       # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
115       # whether the collection is empty, use collection.length.zero? instead of collection.empty?
116       def length
117         load_target.size
118       end
119
120       def empty?
121         size.zero?
122       end
123
124       def any?(&block)
125         if block_given?
126           method_missing(:any?, &block)
127         else
128           !empty?
129         end
130       end
131
132       def uniq(collection = self)
133         seen = Set.new
134         collection.inject([]) do |kept, record|
135           unless seen.include?(record.id)
136             kept << record
137             seen << record.id
138           end
139           kept
140         end
141       end
142
143       # Replace this collection with +other_array+
144       # This will perform a diff and delete/add only records that have changed.
145       def replace(other_array)
146         other_array.each { |val| raise_on_type_mismatch(val) }
147
148         load_target
149         other   = other_array.size < 100 ? other_array : other_array.to_set
150         current = @target.size < 100 ? @target : @target.to_set
151
152         @owner.transaction do
153           delete(@target.select { |v| !other.include?(v) })
154           concat(other_array.select { |v| !current.include?(v) })
155         end
156       end
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           end
166         end
167
168         # overloaded in derived Association classes to provide useful scoping depending on association type.
169         def construct_scope
170           {}
171         end
172
173         def reset_target!
174           @target = Array.new
175         end
176
177         def find_target
178           records =
179             if @reflection.options[:finder_sql]
180               @reflection.klass.find_by_sql(@finder_sql)
181             else
182               find(:all)
183             end
184
185           @reflection.options[:uniq] ? uniq(records) : records
186         end
187
188       private
189
190         def create_record(attrs, &block)
191           ensure_owner_is_not_new
192           record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
193           add_record_to_target_with_callbacks(record, &block)
194         end
195
196         def build_record(attrs, &block)
197           record = @reflection.klass.new(attrs)
198           add_record_to_target_with_callbacks(record, &block)
199         end
200
201         def add_record_to_target_with_callbacks(record)
202           callback(:before_add, record)
203           yield(record) if block_given?
204           @target ||= [] unless loaded?
205           @target << record
206           callback(:after_add, record)
207           record
208         end
209
210         def callback(method, record)
211           callbacks_for(method).each do |callback|
212             case callback
213               when Symbol
214                 @owner.send(callback, record)
215               when Proc, Method
216                 callback.call(@owner, record)
217               else
218                 if callback.respond_to?(method)
219                   callback.send(method, @owner, record)
220                 else
221                   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."
222                 end
223             end
224           end
225         end
226        
227         def callbacks_for(callback_name)
228           full_callback_name = "#{callback_name}_for_#{@reflection.name}"
229           @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
230         end   
231        
232         def ensure_owner_is_not_new
233           if @owner.new_record?
234             raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
235           end
236         end
237                
238     end
239   end
240 end
Note: See TracBrowser for help on using the browser.