activerecord 1.10.1 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +187 -19
- data/RUNNING_UNIT_TESTS +11 -0
- data/lib/active_record.rb +3 -1
- data/lib/active_record/acts/list.rb +25 -14
- data/lib/active_record/acts/nested_set.rb +4 -4
- data/lib/active_record/acts/tree.rb +18 -1
- data/lib/active_record/associations.rb +90 -17
- data/lib/active_record/associations/association_collection.rb +44 -5
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +17 -4
- data/lib/active_record/associations/has_many_association.rb +13 -3
- data/lib/active_record/associations/has_one_association.rb +19 -0
- data/lib/active_record/base.rb +292 -268
- data/lib/active_record/callbacks.rb +14 -14
- data/lib/active_record/connection_adapters/abstract_adapter.rb +137 -75
- data/lib/active_record/connection_adapters/db2_adapter.rb +10 -8
- data/lib/active_record/connection_adapters/mysql_adapter.rb +91 -64
- data/lib/active_record/connection_adapters/oci_adapter.rb +6 -6
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +113 -60
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +15 -12
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +159 -132
- data/lib/active_record/fixtures.rb +59 -12
- data/lib/active_record/locking.rb +10 -9
- data/lib/active_record/migration.rb +112 -5
- data/lib/active_record/query_cache.rb +64 -0
- data/lib/active_record/timestamp.rb +10 -8
- data/lib/active_record/validations.rb +121 -26
- data/rakefile +16 -10
- data/test/aaa_create_tables_test.rb +26 -48
- data/test/abstract_unit.rb +3 -0
- data/test/aggregations_test.rb +19 -19
- data/test/association_callbacks_test.rb +110 -0
- data/test/associations_go_eager_test.rb +48 -14
- data/test/associations_test.rb +344 -142
- data/test/base_test.rb +150 -31
- data/test/binary_test.rb +7 -0
- data/test/callbacks_test.rb +24 -5
- data/test/column_alias_test.rb +2 -2
- data/test/connections/native_sqlserver_odbc/connection.rb +26 -0
- data/test/deprecated_associations_test.rb +27 -28
- data/test/deprecated_finder_test.rb +8 -9
- data/test/finder_test.rb +52 -17
- data/test/fixtures/author.rb +39 -0
- data/test/fixtures/categories.yml +7 -0
- data/test/fixtures/categories_posts.yml +8 -0
- data/test/fixtures/category.rb +2 -0
- data/test/fixtures/comment.rb +3 -1
- data/test/fixtures/comments.yml +43 -1
- data/test/fixtures/companies.yml +14 -0
- data/test/fixtures/company.rb +1 -1
- data/test/fixtures/computers.yml +2 -1
- data/test/fixtures/db_definitions/db2.sql +7 -2
- data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
- data/test/fixtures/db_definitions/mysql.sql +11 -6
- data/test/fixtures/db_definitions/oci.sql +7 -2
- data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
- data/test/fixtures/db_definitions/postgresql.sql +8 -5
- data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite.sql +9 -4
- data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlserver.sql +12 -7
- data/test/fixtures/developer.rb +8 -1
- data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
- data/test/fixtures/post.rb +8 -2
- data/test/fixtures/posts.yml +21 -0
- data/test/fixtures/project.rb +14 -1
- data/test/fixtures/subscriber.rb +3 -0
- data/test/fixtures_test.rb +14 -0
- data/test/inheritance_test.rb +30 -22
- data/test/lifecycle_test.rb +3 -4
- data/test/locking_test.rb +2 -4
- data/test/migration_test.rb +186 -0
- data/test/mixin_nested_set_test.rb +19 -19
- data/test/mixin_test.rb +88 -88
- data/test/modules_test.rb +5 -10
- data/test/multiple_db_test.rb +2 -0
- data/test/pk_test.rb +8 -12
- data/test/reflection_test.rb +8 -4
- data/test/schema_test_postgresql.rb +63 -0
- data/test/thread_safety_test.rb +4 -1
- data/test/transactions_test.rb +9 -2
- data/test/unconnected_test.rb +1 -0
- data/test/validations_test.rb +151 -8
- metadata +11 -5
- data/test/migration_mysql.rb +0 -104
@@ -181,17 +181,17 @@ module ActiveRecord
|
|
181
181
|
|
182
182
|
# Returns a set of itself and all of it's nested children
|
183
183
|
def full_set
|
184
|
-
self.class.
|
184
|
+
self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
|
185
185
|
end
|
186
186
|
|
187
187
|
# Returns a set of all of it's children and nested children
|
188
188
|
def all_children
|
189
|
-
self.class.
|
189
|
+
self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
|
190
190
|
end
|
191
191
|
|
192
192
|
# Returns a set of only this entries immediate children
|
193
193
|
def direct_children
|
194
|
-
self.class.
|
194
|
+
self.class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}")
|
195
195
|
end
|
196
196
|
|
197
197
|
# Prunes a branch off of the tree, shifting all of the elements on the right
|
@@ -209,4 +209,4 @@ module ActiveRecord
|
|
209
209
|
end
|
210
210
|
end
|
211
211
|
end
|
212
|
-
end
|
212
|
+
end
|
@@ -35,9 +35,26 @@ module ActiveRecord
|
|
35
35
|
def acts_as_tree(options = {})
|
36
36
|
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
|
37
37
|
configuration.update(options) if options.is_a?(Hash)
|
38
|
-
|
38
|
+
|
39
39
|
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
|
40
40
|
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => true
|
41
|
+
|
42
|
+
module_eval <<-END
|
43
|
+
def self.roots
|
44
|
+
self.find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => "#{configuration[:order]}")
|
45
|
+
end
|
46
|
+
def self.root
|
47
|
+
self.find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => "#{configuration[:order]}")
|
48
|
+
end
|
49
|
+
END
|
50
|
+
|
51
|
+
define_method(:siblings) do
|
52
|
+
if parent
|
53
|
+
self.class.find(:all, :conditions => [ "#{configuration[:foreign_key]} = ?", parent.id ], :order => configuration[:order])
|
54
|
+
else
|
55
|
+
self.class.roots
|
56
|
+
end
|
57
|
+
end
|
41
58
|
end
|
42
59
|
end
|
43
60
|
end
|
@@ -17,7 +17,7 @@ module ActiveRecord
|
|
17
17
|
def clear_association_cache #:nodoc:
|
18
18
|
self.class.reflect_on_all_associations.to_a.each do |assoc|
|
19
19
|
instance_variable_set "@#{assoc.name}", nil
|
20
|
-
end
|
20
|
+
end unless self.new_record?
|
21
21
|
end
|
22
22
|
|
23
23
|
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
|
@@ -43,7 +43,7 @@ module ActiveRecord
|
|
43
43
|
#
|
44
44
|
# == Example
|
45
45
|
#
|
46
|
-
# link
|
46
|
+
# link:files/examples/associations.png
|
47
47
|
#
|
48
48
|
# == Is it belongs_to or has_one?
|
49
49
|
#
|
@@ -96,6 +96,30 @@ module ActiveRecord
|
|
96
96
|
# * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
|
97
97
|
# * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
|
98
98
|
#
|
99
|
+
# === Association callbacks
|
100
|
+
#
|
101
|
+
# Similiar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
|
102
|
+
# trigged when you add an object to or removing an object from a association collection. Example:
|
103
|
+
#
|
104
|
+
# class Project
|
105
|
+
# has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
|
106
|
+
#
|
107
|
+
# def evaluate_velocity(developer)
|
108
|
+
# ...
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# It's possible to stack callbacks by passing them as an array. Example:
|
113
|
+
#
|
114
|
+
# class Project
|
115
|
+
# has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# Possible callbacks are: before_add, after_add, before_remove and after_remove.
|
119
|
+
#
|
120
|
+
# Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with
|
121
|
+
# the before_remove callbacks, if an exception is thrown the object doesn't get removed.
|
122
|
+
#
|
99
123
|
# == Caching
|
100
124
|
#
|
101
125
|
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
@@ -150,7 +174,8 @@ module ActiveRecord
|
|
150
174
|
# in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
|
151
175
|
# you alter the :order and :conditions on the association definitions themselves.
|
152
176
|
#
|
153
|
-
# It's currently not possible to use eager loading on multiple associations from the same table.
|
177
|
+
# It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will also not pull
|
178
|
+
# additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
|
154
179
|
#
|
155
180
|
# == Modules
|
156
181
|
#
|
@@ -199,6 +224,8 @@ module ActiveRecord
|
|
199
224
|
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
200
225
|
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
|
201
226
|
# This will also destroy the objects if they're declared as belongs_to and dependent on this model.
|
227
|
+
# * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
|
228
|
+
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
|
202
229
|
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
203
230
|
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
204
231
|
# * <tt>collection.size</tt> - returns the number of associated objects.
|
@@ -214,12 +241,14 @@ module ActiveRecord
|
|
214
241
|
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
|
215
242
|
# * <tt>Firm#clients<<</tt>
|
216
243
|
# * <tt>Firm#clients.delete</tt>
|
244
|
+
# * <tt>Firm#clients=</tt>
|
245
|
+
# * <tt>Firm#client_ids=</tt>
|
217
246
|
# * <tt>Firm#clients.clear</tt>
|
218
247
|
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
219
248
|
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
220
249
|
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
221
250
|
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
222
|
-
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("
|
251
|
+
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
223
252
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
224
253
|
#
|
225
254
|
# Options are:
|
@@ -254,7 +283,8 @@ module ActiveRecord
|
|
254
283
|
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
255
284
|
# 'ORDER BY p.first_name'
|
256
285
|
def has_many(association_id, options = {})
|
257
|
-
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql
|
286
|
+
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql,
|
287
|
+
:before_add, :after_add, :before_remove, :after_remove ], options.keys)
|
258
288
|
association_name, association_class_name, association_class_primary_key_name =
|
259
289
|
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
260
290
|
|
@@ -271,7 +301,8 @@ module ActiveRecord
|
|
271
301
|
end
|
272
302
|
|
273
303
|
add_multiple_associated_save_callbacks(association_name)
|
274
|
-
|
304
|
+
add_association_callbacks(association_name, options)
|
305
|
+
|
275
306
|
collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
|
276
307
|
|
277
308
|
# deprecated api
|
@@ -299,7 +330,7 @@ module ActiveRecord
|
|
299
330
|
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
|
300
331
|
#
|
301
332
|
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
302
|
-
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.
|
333
|
+
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
|
303
334
|
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
304
335
|
# * <tt>Account#beneficiary.nil?</tt>
|
305
336
|
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
@@ -371,8 +402,8 @@ module ActiveRecord
|
|
371
402
|
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
372
403
|
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
373
404
|
# * <tt>Post#author.nil?</tt>
|
374
|
-
# * <tt>Post#build_author</tt> (similar to <tt>Author.new
|
375
|
-
# * <tt>Post#create_author</tt> (similar to <tt>
|
405
|
+
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
406
|
+
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
376
407
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
377
408
|
#
|
378
409
|
# Options are:
|
@@ -447,7 +478,6 @@ module ActiveRecord
|
|
447
478
|
# that you want available on retrieval. Note that any fields in the join table will override matching field names
|
448
479
|
# in the two joined tables. As a consequence, having an "id" field in the join table usually has the undesirable
|
449
480
|
# result of clobbering the "id" fields in either of the other two tables.
|
450
|
-
#
|
451
481
|
#
|
452
482
|
# Adds the following methods for retrieval and query.
|
453
483
|
# +collection+ is replaced with the symbol passed as the first argument, so
|
@@ -462,6 +492,8 @@ module ActiveRecord
|
|
462
492
|
# (collection.concat_with_attributes is an alias to this method).
|
463
493
|
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
|
464
494
|
# This does not destroy the objects.
|
495
|
+
# * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
|
496
|
+
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
|
465
497
|
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
466
498
|
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
467
499
|
# * <tt>collection.size</tt> - returns the number of associated objects.
|
@@ -473,6 +505,8 @@ module ActiveRecord
|
|
473
505
|
# * <tt>Developer#projects<<</tt>
|
474
506
|
# * <tt>Developer#projects.push_with_attributes</tt>
|
475
507
|
# * <tt>Developer#projects.delete</tt>
|
508
|
+
# * <tt>Developer#projects=</tt>
|
509
|
+
# * <tt>Developer#project_ids=</tt>
|
476
510
|
# * <tt>Developer#projects.clear</tt>
|
477
511
|
# * <tt>Developer#projects.empty?</tt>
|
478
512
|
# * <tt>Developer#projects.size</tt>
|
@@ -506,9 +540,12 @@ module ActiveRecord
|
|
506
540
|
# has_and_belongs_to_many :projects
|
507
541
|
# has_and_belongs_to_many :nations, :class_name => "Country"
|
508
542
|
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
|
543
|
+
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
|
544
|
+
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
|
509
545
|
def has_and_belongs_to_many(association_id, options = {})
|
510
546
|
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
|
511
|
-
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq
|
547
|
+
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
|
548
|
+
:before_remove, :after_remove ], options.keys)
|
512
549
|
association_name, association_class_name, association_class_primary_key_name =
|
513
550
|
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
514
551
|
|
@@ -522,6 +559,7 @@ module ActiveRecord
|
|
522
559
|
|
523
560
|
before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
|
524
561
|
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
|
562
|
+
add_association_callbacks(association_name, options)
|
525
563
|
|
526
564
|
# deprecated api
|
527
565
|
deprecated_collection_count_method(association_name)
|
@@ -631,6 +669,10 @@ module ActiveRecord
|
|
631
669
|
association.replace(new_value)
|
632
670
|
association
|
633
671
|
end
|
672
|
+
|
673
|
+
define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
|
674
|
+
send("#{association_name}=", association_class_name.constantize.find(new_value))
|
675
|
+
end
|
634
676
|
end
|
635
677
|
|
636
678
|
def require_association_class(class_name)
|
@@ -656,7 +698,7 @@ module ActiveRecord
|
|
656
698
|
end
|
657
699
|
|
658
700
|
module_eval do
|
659
|
-
|
701
|
+
after_callback = <<-end_eval
|
660
702
|
association = instance_variable_get("@#{association_name}")
|
661
703
|
if association.respond_to?(:loaded?)
|
662
704
|
if @new_record_before_save
|
@@ -668,13 +710,18 @@ module ActiveRecord
|
|
668
710
|
association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
|
669
711
|
end
|
670
712
|
end_eval
|
713
|
+
|
714
|
+
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
715
|
+
after_create(after_callback)
|
716
|
+
after_update(after_callback)
|
671
717
|
end
|
672
718
|
end
|
673
719
|
|
674
720
|
def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
|
675
721
|
define_method("#{constructor}_#{association_name}") do |*params|
|
676
|
-
attributees
|
677
|
-
|
722
|
+
attributees = params.first unless params.empty?
|
723
|
+
replace_existing = params[1].nil? ? true : params[1]
|
724
|
+
association = instance_variable_get("@#{association_name}")
|
678
725
|
|
679
726
|
if association.nil?
|
680
727
|
association = association_proxy_class.new(self,
|
@@ -683,7 +730,11 @@ module ActiveRecord
|
|
683
730
|
instance_variable_set("@#{association_name}", association)
|
684
731
|
end
|
685
732
|
|
686
|
-
|
733
|
+
if association_proxy_class == HasOneAssociation
|
734
|
+
association.send(constructor, attributees, replace_existing)
|
735
|
+
else
|
736
|
+
association.send(constructor, attributees)
|
737
|
+
end
|
687
738
|
end
|
688
739
|
end
|
689
740
|
|
@@ -715,7 +766,7 @@ module ActiveRecord
|
|
715
766
|
next unless row[primary_key_table[reflection.table_name]]
|
716
767
|
|
717
768
|
record.send(
|
718
|
-
"#{reflection.name}
|
769
|
+
"set_#{reflection.name}_target",
|
719
770
|
reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
|
720
771
|
)
|
721
772
|
end
|
@@ -770,11 +821,21 @@ module ActiveRecord
|
|
770
821
|
sql << reflections.collect { |reflection| association_join(reflection) }.to_s
|
771
822
|
sql << "#{options[:joins]} " if options[:joins]
|
772
823
|
add_conditions!(sql, options[:conditions])
|
824
|
+
add_sti_conditions!(sql, reflections)
|
773
825
|
sql << "ORDER BY #{options[:order]} " if options[:order]
|
774
826
|
|
775
827
|
return sanitize_sql(sql)
|
776
828
|
end
|
777
829
|
|
830
|
+
def add_sti_conditions!(sql, reflections)
|
831
|
+
sti_sql = ""
|
832
|
+
reflections.each do |reflection|
|
833
|
+
sti_sql << " AND #{reflection.klass.send(:type_condition)}" unless reflection.klass.descends_from_active_record?
|
834
|
+
end
|
835
|
+
sti_sql.sub!(/AND/, "WHERE") unless sql =~ /where/i
|
836
|
+
sql << sti_sql
|
837
|
+
end
|
838
|
+
|
778
839
|
def column_aliases(schema_abbreviations)
|
779
840
|
schema_abbreviations.collect { |cn, tc| "#{tc.join(".")} AS #{cn}" }.join(", ")
|
780
841
|
end
|
@@ -786,7 +847,7 @@ module ActiveRecord
|
|
786
847
|
"#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
|
787
848
|
"#{table_name}.#{primary_key} " +
|
788
849
|
" LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
|
789
|
-
"#{reflection.options[:join_table]}.#{reflection.options[:
|
850
|
+
"#{reflection.options[:join_table]}.#{reflection.options[:association_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " +
|
790
851
|
"#{reflection.klass.table_name}.#{reflection.klass.primary_key} "
|
791
852
|
when :has_many, :has_one
|
792
853
|
" LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
|
@@ -801,6 +862,18 @@ module ActiveRecord
|
|
801
862
|
end
|
802
863
|
end
|
803
864
|
|
865
|
+
def add_association_callbacks(association_name, options)
|
866
|
+
callbacks = %w(before_add after_add before_remove after_remove)
|
867
|
+
callbacks.each do |callback_name|
|
868
|
+
full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
|
869
|
+
defined_callbacks = options[callback_name.to_sym]
|
870
|
+
if options.has_key?(callback_name.to_sym)
|
871
|
+
callback_array = defined_callbacks.kind_of?(Array) ? defined_callbacks : [defined_callbacks]
|
872
|
+
class_inheritable_reader full_callback_name.to_sym
|
873
|
+
write_inheritable_array(full_callback_name.to_sym, callback_array)
|
874
|
+
end
|
875
|
+
end
|
876
|
+
end
|
804
877
|
|
805
878
|
def extract_record(schema_abbreviations, table_name, row)
|
806
879
|
record = {}
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
module Associations
|
3
5
|
class AssociationCollection < AssociationProxy #:nodoc:
|
@@ -19,11 +21,13 @@ module ActiveRecord
|
|
19
21
|
@owner.transaction do
|
20
22
|
flatten_deeper(records).each do |record|
|
21
23
|
raise_on_type_mismatch(record)
|
24
|
+
callback(:before_add, record)
|
22
25
|
result &&= insert_record(record) unless @owner.new_record?
|
23
26
|
@target << record
|
27
|
+
callback(:after_add, record)
|
24
28
|
end
|
25
29
|
end
|
26
|
-
|
30
|
+
|
27
31
|
result and self
|
28
32
|
end
|
29
33
|
|
@@ -38,8 +42,12 @@ module ActiveRecord
|
|
38
42
|
return if records.empty?
|
39
43
|
|
40
44
|
@owner.transaction do
|
45
|
+
records.each { |record| callback(:before_remove, record) }
|
41
46
|
delete_records(records)
|
42
|
-
records.each
|
47
|
+
records.each do |record|
|
48
|
+
@target.delete(record)
|
49
|
+
callback(:after_remove, record)
|
50
|
+
end
|
43
51
|
end
|
44
52
|
end
|
45
53
|
|
@@ -83,11 +91,19 @@ module ActiveRecord
|
|
83
91
|
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
|
84
92
|
end
|
85
93
|
|
94
|
+
# Replace this collection with +other_array+
|
95
|
+
# This will perform a diff and delete/add only records that have changed.
|
86
96
|
def replace(other_array)
|
87
|
-
other_array.each{ |val| raise_on_type_mismatch(val) }
|
97
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
88
98
|
|
89
|
-
|
90
|
-
|
99
|
+
load_target
|
100
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
101
|
+
current = @target.size < 100 ? @target : @target.to_set
|
102
|
+
|
103
|
+
@owner.transaction do
|
104
|
+
delete(@target.select { |v| !other.include?(v) })
|
105
|
+
concat(other_array.select { |v| !current.include?(v) })
|
106
|
+
end
|
91
107
|
end
|
92
108
|
|
93
109
|
private
|
@@ -103,6 +119,29 @@ module ActiveRecord
|
|
103
119
|
def flatten_deeper(array)
|
104
120
|
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
105
121
|
end
|
122
|
+
|
123
|
+
def callback(method, record)
|
124
|
+
callbacks_for(method).each do |callback|
|
125
|
+
case callback
|
126
|
+
when Symbol
|
127
|
+
@owner.send(callback, record)
|
128
|
+
when Proc, Method
|
129
|
+
callback.call(@owner, record)
|
130
|
+
else
|
131
|
+
if callback.respond_to?(method)
|
132
|
+
callback.send(method, @owner, record)
|
133
|
+
else
|
134
|
+
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."
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def callbacks_for(callback_name)
|
141
|
+
full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}"
|
142
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) or []
|
143
|
+
end
|
144
|
+
|
106
145
|
end
|
107
146
|
end
|
108
147
|
end
|
@@ -66,7 +66,7 @@ module ActiveRecord
|
|
66
66
|
# Otherwise, construct a query.
|
67
67
|
else
|
68
68
|
ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
|
69
|
-
records = find_target(@finder_sql.sub(/(ORDER BY|$)/, "AND j.#{@association_foreign_key} IN (#{ids_list}) \\1"))
|
69
|
+
records = find_target(@finder_sql.sub(/(ORDER BY|$)/, " AND j.#{@association_foreign_key} IN (#{ids_list}) \\1"))
|
70
70
|
if records.size == ids.size
|
71
71
|
if ids.size == 1 and !expects_array
|
72
72
|
records.first
|
@@ -82,8 +82,10 @@ module ActiveRecord
|
|
82
82
|
def push_with_attributes(record, join_attributes = {})
|
83
83
|
raise_on_type_mismatch(record)
|
84
84
|
join_attributes.each { |key, value| record[key.to_s] = value }
|
85
|
+
callback(:before_add, record)
|
85
86
|
insert_record(record) unless @owner.new_record?
|
86
87
|
@target << record
|
88
|
+
callback(:after_add, record)
|
87
89
|
self
|
88
90
|
end
|
89
91
|
|
@@ -104,7 +106,9 @@ module ActiveRecord
|
|
104
106
|
end
|
105
107
|
|
106
108
|
def insert_record(record)
|
107
|
-
|
109
|
+
if record.new_record?
|
110
|
+
return false unless record.save
|
111
|
+
end
|
108
112
|
|
109
113
|
if @options[:insert_sql]
|
110
114
|
@owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
|
@@ -136,7 +140,7 @@ module ActiveRecord
|
|
136
140
|
|
137
141
|
def delete_records(records)
|
138
142
|
if sql = @options[:delete_sql]
|
139
|
-
records.each { |record| @owner.connection.execute(sql) }
|
143
|
+
records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
|
140
144
|
else
|
141
145
|
ids = quoted_record_ids(records)
|
142
146
|
sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})"
|
@@ -145,7 +149,7 @@ module ActiveRecord
|
|
145
149
|
end
|
146
150
|
|
147
151
|
def construct_sql
|
148
|
-
interpolate_sql_options!(@options, :finder_sql
|
152
|
+
interpolate_sql_options!(@options, :finder_sql)
|
149
153
|
|
150
154
|
if @options[:finder_sql]
|
151
155
|
@finder_sql = @options[:finder_sql]
|
@@ -156,6 +160,15 @@ module ActiveRecord
|
|
156
160
|
"j.#{@association_class_primary_key_name} = #{@owner.quoted_id} "
|
157
161
|
|
158
162
|
@finder_sql << " AND #{interpolate_sql(@options[:conditions])}" if @options[:conditions]
|
163
|
+
|
164
|
+
unless @association_class.descends_from_active_record?
|
165
|
+
type_condition = @association_class.send(:subclasses).inject("t.#{@association_class.inheritance_column} = '#{@association_class.name.demodulize}' ") do |condition, subclass|
|
166
|
+
condition << "OR t.#{@association_class.inheritance_column} = '#{subclass.name.demodulize}' "
|
167
|
+
end
|
168
|
+
|
169
|
+
@finder_sql << " AND (#{type_condition})"
|
170
|
+
end
|
171
|
+
|
159
172
|
@finder_sql << " ORDER BY #{@order}" if @order
|
160
173
|
end
|
161
174
|
end
|