activerecord 1.4.0 → 1.5.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.

Files changed (55) hide show
  1. data/CHANGELOG +98 -0
  2. data/install.rb +1 -0
  3. data/lib/active_record.rb +1 -0
  4. data/lib/active_record/acts/list.rb +19 -16
  5. data/lib/active_record/associations.rb +164 -164
  6. data/lib/active_record/associations/association_collection.rb +44 -71
  7. data/lib/active_record/associations/association_proxy.rb +76 -0
  8. data/lib/active_record/associations/belongs_to_association.rb +74 -0
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +34 -21
  10. data/lib/active_record/associations/has_many_association.rb +34 -30
  11. data/lib/active_record/associations/has_one_association.rb +48 -0
  12. data/lib/active_record/base.rb +62 -18
  13. data/lib/active_record/callbacks.rb +17 -8
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +11 -10
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -1
  17. data/lib/active_record/connection_adapters/sqlite_adapter.rb +94 -73
  18. data/lib/active_record/deprecated_associations.rb +46 -8
  19. data/lib/active_record/fixtures.rb +1 -1
  20. data/lib/active_record/observer.rb +5 -1
  21. data/lib/active_record/support/binding_of_caller.rb +72 -68
  22. data/lib/active_record/support/breakpoint.rb +526 -524
  23. data/lib/active_record/support/class_inheritable_attributes.rb +105 -29
  24. data/lib/active_record/support/core_ext.rb +1 -0
  25. data/lib/active_record/support/core_ext/hash.rb +5 -0
  26. data/lib/active_record/support/core_ext/hash/keys.rb +35 -0
  27. data/lib/active_record/support/core_ext/numeric.rb +7 -0
  28. data/lib/active_record/support/core_ext/numeric/bytes.rb +33 -0
  29. data/lib/active_record/support/core_ext/numeric/time.rb +59 -0
  30. data/lib/active_record/support/core_ext/string.rb +5 -0
  31. data/lib/active_record/support/core_ext/string/inflections.rb +41 -0
  32. data/lib/active_record/support/dependencies.rb +1 -14
  33. data/lib/active_record/support/inflector.rb +6 -6
  34. data/lib/active_record/support/misc.rb +0 -24
  35. data/lib/active_record/validations.rb +34 -1
  36. data/lib/active_record/vendor/mysql411.rb +305 -0
  37. data/rakefile +11 -2
  38. data/test/abstract_unit.rb +1 -2
  39. data/test/associations_test.rb +234 -23
  40. data/test/base_test.rb +50 -1
  41. data/test/callbacks_test.rb +16 -0
  42. data/test/connections/native_mysql/connection.rb +2 -2
  43. data/test/connections/native_sqlite3/connection.rb +34 -0
  44. data/test/deprecated_associations_test.rb +36 -2
  45. data/test/fixtures/company.rb +2 -0
  46. data/test/fixtures/computer.rb +3 -0
  47. data/test/fixtures/computers.yml +3 -0
  48. data/test/fixtures/db_definitions/db2.sql +5 -0
  49. data/test/fixtures/db_definitions/mysql.sql +5 -0
  50. data/test/fixtures/db_definitions/postgresql.sql +5 -0
  51. data/test/fixtures/db_definitions/sqlite.sql +5 -0
  52. data/test/fixtures/db_definitions/sqlserver.sql +5 -1
  53. data/test/fixtures/fixture_database.sqlite +0 -0
  54. data/test/validations_test.rb +21 -0
  55. metadata +22 -2
data/CHANGELOG CHANGED
@@ -1,3 +1,101 @@
1
+ *1.5.0* (January 17th, 2005)
2
+
3
+ * Fixed that unit tests for MySQL are now run as the "rails" user instead of root #455 [Eric Hodel]
4
+
5
+ * Added validates_associated that enables validation of objects in an unsaved association #398 [Tim Bates]. Example:
6
+
7
+ class Book < ActiveRecord::Base
8
+ has_many :pages
9
+ belongs_to :library
10
+
11
+ validates_associated :pages, :library
12
+ end
13
+
14
+ * Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition:
15
+
16
+ == Unsaved objects and associations
17
+
18
+ You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
19
+ aware of, mostly involving the saving of associated objects.
20
+
21
+ === One-to-one associations
22
+
23
+ * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in
24
+ order to update their primary keys - except if the parent object is unsaved (new_record? == true).
25
+ * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
26
+ is cancelled.
27
+ * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
28
+ * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does
29
+ not save the parent either.
30
+
31
+ === Collections
32
+
33
+ * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object
34
+ (the owner of the collection) is not yet stored in the database.
35
+ * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
36
+ * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
37
+ * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
38
+
39
+ * Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates]
40
+
41
+ * Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates]
42
+
43
+ * Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 [Tim Bates]
44
+
45
+ * Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates]
46
+
47
+ * Fixed binary support for PostgreSQL #444 [alex@byzantine.no]
48
+
49
+ * Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the
50
+ collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If
51
+ it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards,
52
+ it'll take one less SELECT query if you use length.
53
+
54
+ * Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 [atyp.de]
55
+
56
+ * Fixed that foreign keys named the same as the association would cause stack overflow #437 [Eric Anderson]
57
+
58
+ * Fixed default scope of acts_as_list from "1" to "1 = 1", so it'll work in PostgreSQL (among other places) #427 [Alexey]
59
+
60
+ * Added Base#reload that reloads the attributes of an object from the database #422 [Andreas Schwarz]
61
+
62
+ * Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 [bitsweat]
63
+
64
+ * Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower]
65
+
66
+ * Added that Observers can use the observes class method instead of overwriting self.observed_class().
67
+
68
+ Before:
69
+ class ListSweeper < ActiveRecord::Base
70
+ def self.observed_class() [ List, Item ]
71
+ end
72
+
73
+ After:
74
+ class ListSweeper < ActiveRecord::Base
75
+ observes List, Item
76
+ end
77
+
78
+ * Fixed that conditions in has_many and has_and_belongs_to_many should be interpolated just like the finder_sql is
79
+
80
+ * Fixed Base#update_attribute to be indifferent to whether a string or symbol is used to describe the name
81
+
82
+ * Added Base#toggle(attribute) and Base#toggle!(attribute) that makes it easier to flip a switch or flag.
83
+
84
+ Before: topic.update_attribute(:approved, !approved?)
85
+ After : topic.toggle!(:approved)
86
+
87
+ * Added Base#increment!(attribute) and Base#decrement!(attribute) that also saves the records. Example:
88
+
89
+ page.views # => 1
90
+ page.increment!(:views) # executes an UPDATE statement
91
+ page.views # => 2
92
+
93
+ page.increment(:views).increment!(:views)
94
+ page.views # => 4
95
+
96
+ * Added Base#increment(attribute) and Base#decrement(attribute) that encapsulates the += 1 and -= 1 patterns.
97
+
98
+
1
99
  *1.4.0* (January 4th, 2005)
2
100
 
3
101
  * Added automated optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
data/install.rb CHANGED
@@ -56,6 +56,7 @@ files = %w-
56
56
  active_record/transactions.rb
57
57
  active_record/validations.rb
58
58
  active_record/vendor/mysql.rb
59
+ active_record/vendor/mysql411.rb
59
60
  active_record/vendor/simple.rb
60
61
  -
61
62
 
@@ -24,6 +24,7 @@
24
24
 
25
25
  $:.unshift(File.dirname(__FILE__))
26
26
 
27
+ require 'active_record/support/core_ext'
27
28
  require 'active_record/support/clean_logger'
28
29
  require 'active_record/support/misc'
29
30
  require 'active_record/support/dependencies'
@@ -27,11 +27,12 @@ module ActiveRecord
27
27
  # Configuration options are:
28
28
  #
29
29
  # * +column+ - specifies the column name to use for keeping the position integer (default: position)
30
- # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" (if that hasn't been already) and use that
31
- # as the foreign key restriction. It's also possible to give it an entire string that is interpolated if you need a tighter scope than
32
- # just a foreign key. Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
30
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
31
+ # (if that hasn't been already) and use that as the foreign key restriction. It's also possible
32
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
33
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
34
  def acts_as_list(options = {})
34
- configuration = { :column => "position", :scope => "1" }
35
+ configuration = { :column => "position", :scope => "1 = 1" }
35
36
  configuration.update(options) if options.is_a?(Hash)
36
37
 
37
38
  configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
@@ -65,7 +66,10 @@ module ActiveRecord
65
66
  end
66
67
  end
67
68
 
68
- # All the methods available to a record that has had <tt>acts_as_list</tt> specified.
69
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
70
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
71
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
72
+ # the first in the list of all chapters.
69
73
  module InstanceMethods
70
74
  def move_lower
71
75
  return unless lower_item
@@ -75,7 +79,7 @@ module ActiveRecord
75
79
  increment_position
76
80
  end
77
81
  end
78
-
82
+
79
83
  def move_higher
80
84
  return unless higher_item
81
85
 
@@ -84,14 +88,14 @@ module ActiveRecord
84
88
  decrement_position
85
89
  end
86
90
  end
87
-
91
+
88
92
  def move_to_bottom
89
93
  self.class.transaction do
90
94
  decrement_positions_on_lower_items
91
95
  assume_bottom_position
92
96
  end
93
97
  end
94
-
98
+
95
99
  def move_to_top
96
100
  self.class.transaction do
97
101
  increment_positions_on_higher_items
@@ -99,7 +103,6 @@ module ActiveRecord
99
103
  end
100
104
  end
101
105
 
102
-
103
106
  def remove_from_list
104
107
  decrement_positions_on_lower_items
105
108
  end
@@ -113,11 +116,10 @@ module ActiveRecord
113
116
  update_attribute position_column, self.send(position_column).to_i - 1
114
117
  end
115
118
 
116
-
117
119
  def first?
118
120
  self.send(position_column) == 1
119
121
  end
120
-
122
+
121
123
  def last?
122
124
  self.send(position_column) == bottom_position_in_list
123
125
  end
@@ -140,9 +142,9 @@ module ActiveRecord
140
142
  end
141
143
 
142
144
  def add_to_list_bottom
143
- write_attribute(position_column, bottom_position_in_list.to_i + 1)
145
+ self[position_column] = bottom_position_in_list.to_i + 1
144
146
  end
145
-
147
+
146
148
  # Overwrite this method to define the scope of the list changes
147
149
  def scope_condition() "1" end
148
150
 
@@ -159,13 +161,14 @@ module ActiveRecord
159
161
  end
160
162
 
161
163
  def assume_bottom_position
162
- update_attribute position_column, bottom_position_in_list.to_i + 1
164
+ update_attribute(position_column, bottom_position_in_list.to_i + 1)
163
165
  end
164
166
 
165
167
  def assume_top_position
166
- update_attribute position_column, 1
168
+ update_attribute(position_column, 1)
167
169
  end
168
-
170
+
171
+ # This has the effect of moving all the lower items up one.
169
172
  def decrement_positions_on_lower_items
170
173
  self.class.update_all(
171
174
  "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
@@ -1,4 +1,7 @@
1
+ require 'active_record/associations/association_proxy'
1
2
  require 'active_record/associations/association_collection'
3
+ require 'active_record/associations/belongs_to_association'
4
+ require 'active_record/associations/has_one_association'
2
5
  require 'active_record/associations/has_many_association'
3
6
  require 'active_record/associations/has_and_belongs_to_many_association'
4
7
  require 'active_record/deprecated_associations'
@@ -30,9 +33,9 @@ module ActiveRecord
30
33
  # end
31
34
  #
32
35
  # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
33
- # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
36
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
34
37
  # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
35
- # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
38
+ # <tt>Project#project_manager.build, Project#project_manager.create</tt>
36
39
  # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
37
40
  # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
38
41
  # <tt>Project#milestones.build, Project#milestones.create</tt>
@@ -71,6 +74,29 @@ module ActiveRecord
71
74
  # PRIMARY KEY (id)
72
75
  # )
73
76
  #
77
+ # == Unsaved objects and associations
78
+ #
79
+ # You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
80
+ # aware of, mostly involving the saving of associated objects.
81
+ #
82
+ # === One-to-one associations
83
+ #
84
+ # * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in
85
+ # order to update their primary keys - except if the parent object is unsaved (new_record? == true).
86
+ # * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
87
+ # is cancelled.
88
+ # * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
89
+ # * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does
90
+ # not save the parent either.
91
+ #
92
+ # === Collections
93
+ #
94
+ # * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object
95
+ # (the owner of the collection) is not yet stored in the database.
96
+ # * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
97
+ # * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
98
+ # * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
99
+ #
74
100
  # == Caching
75
101
  #
76
102
  # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@@ -124,7 +150,7 @@ module ActiveRecord
124
150
  module ClassMethods
125
151
  # Adds the following methods for retrieval and query of collections of associated objects.
126
152
  # +collection+ is replaced with the symbol passed as the first argument, so
127
- # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
153
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
128
154
  # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
129
155
  # An empty array is returned if none are found.
130
156
  # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
@@ -200,18 +226,9 @@ module ActiveRecord
200
226
  module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
201
227
  end
202
228
 
203
- define_method(association_name) do |*params|
204
- force_reload = params.first unless params.empty?
205
- association = instance_variable_get("@#{association_name}")
206
- if association.nil?
207
- association = HasManyAssociation.new(self,
208
- association_name, association_class_name,
209
- association_class_primary_key_name, options)
210
- instance_variable_set("@#{association_name}", association)
211
- end
212
- association.reload if force_reload
213
- association
214
- end
229
+ add_multiple_associated_save_callbacks(association_name)
230
+
231
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
215
232
 
216
233
  # deprecated api
217
234
  deprecated_collection_count_method(association_name)
@@ -220,31 +237,28 @@ module ActiveRecord
220
237
  deprecated_has_collection_method(association_name)
221
238
  deprecated_find_in_collection_method(association_name)
222
239
  deprecated_find_all_in_collection_method(association_name)
223
- deprecated_create_method(association_name)
224
- deprecated_build_method(association_name)
240
+ deprecated_collection_create_method(association_name)
241
+ deprecated_collection_build_method(association_name)
225
242
  end
226
243
 
227
244
  # Adds the following methods for retrieval and query of a single associated object.
228
245
  # +association+ is replaced with the symbol passed as the first argument, so
229
- # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
246
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
230
247
  # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
231
248
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
232
249
  # and saves the associate object.
233
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
234
- # same id as the associated object.
235
250
  # * <tt>association.nil?</tt> - returns true if there is no associated object.
236
- # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
251
+ # * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
237
252
  # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
238
- # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
253
+ # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
239
254
  # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
240
255
  #
241
256
  # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
242
257
  # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
243
258
  # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
244
- # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
245
259
  # * <tt>Account#beneficiary.nil?</tt>
246
- # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
247
- # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
260
+ # * <tt>Account#beneficiary.build</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
261
+ # * <tt>Account#beneficiary.create</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
248
262
  # The declaration can also include an options hash to specialize the behavior of the association.
249
263
  #
250
264
  # Options are:
@@ -265,35 +279,53 @@ module ActiveRecord
265
279
  # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
266
280
  # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
267
281
  def has_one(association_id, options = {})
268
- options.merge!({ :remote => true })
269
- belongs_to(association_id, options)
282
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
270
283
 
271
- association_name, association_class_name, class_primary_key_name =
284
+ association_name, association_class_name, association_class_primary_key_name =
272
285
  associate_identification(association_id, options[:class_name], options[:foreign_key], false)
273
286
 
274
287
  require_association_class(association_class_name)
275
288
 
276
- has_one_writer_method(association_name, association_class_name, class_primary_key_name)
277
- build_method("build_", association_name, association_class_name, class_primary_key_name)
278
- create_method("create_", association_name, association_class_name, class_primary_key_name)
289
+ module_eval do
290
+ after_save <<-EOF
291
+ association = instance_variable_get("@#{association_name}")
292
+ if (true or @new_record_before_save) and association.respond_to?(:loaded?) and not association.nil?
293
+ association["#{association_class_primary_key_name}"] = id
294
+ association.save(true)
295
+ association.send(:construct_sql)
296
+ end
297
+ EOF
298
+ end
299
+
300
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
279
301
 
280
- module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
302
+ module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent]
303
+
304
+ # deprecated api
305
+ deprecated_has_association_method(association_name)
306
+ deprecated_build_method("build_", association_name, association_class_name, association_class_primary_key_name)
307
+ deprecated_create_method("create_", association_name, association_class_name, association_class_primary_key_name)
308
+ deprecated_association_comparison_method(association_name, association_class_name)
281
309
  end
282
310
 
283
311
  # Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
284
312
  # +association+ is replaced with the symbol passed as the first argument, so
285
- # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
313
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
286
314
  # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
287
315
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
288
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
289
- # same id as the associated object.
290
316
  # * <tt>association.nil?</tt> - returns true if there is no associated object.
317
+ # * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
318
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
319
+ # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
320
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
291
321
  #
292
322
  # Example: A Post class declares <tt>has_one :author</tt>, which will add:
293
323
  # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
294
324
  # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
295
325
  # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
296
326
  # * <tt>Post#author.nil?</tt>
327
+ # * <tt>Post#author.build</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
328
+ # * <tt>Post#author.create</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
297
329
  # The declaration can also include an options hash to specialize the behavior of the association.
298
330
  #
299
331
  # Options are:
@@ -317,47 +349,46 @@ module ActiveRecord
317
349
  # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
318
350
  # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
319
351
  # :conditions => 'discounts > #{payments_count}'
320
- def belongs_to(association_id, options = {})
321
- validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
352
+ def belongs_to(association_id, options = {})
353
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
322
354
 
323
- association_name, association_class_name, class_primary_key_name =
324
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
355
+ association_name, association_class_name, class_primary_key_name =
356
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
325
357
 
326
- require_association_class(association_class_name)
358
+ require_association_class(association_class_name)
327
359
 
328
- association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
360
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
329
361
 
330
- if options[:remote]
331
- association_finder = <<-"end_eval"
332
- #{association_class_name}.find_first(
333
- "#{class_primary_key_name} = \#{quoted_id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
334
- #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
335
- )
336
- end_eval
337
- else
338
- association_finder = options[:conditions] ?
339
- "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
340
- "#{association_class_name}.find(#{association_class_primary_key_name})"
341
- end
342
-
343
- has_association_method(association_name)
344
- association_reader_method(association_name, association_finder)
345
- belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
346
- association_comparison_method(association_name, association_class_name)
362
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
347
363
 
348
- if options[:counter_cache]
349
- module_eval(
350
- "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
351
- " if has_#{association_name}?'"
352
- )
364
+ module_eval do
365
+ before_save <<-EOF
366
+ association = instance_variable_get("@#{association_name}")
367
+ if association.respond_to?(:loaded?) and not association.nil? and association.new_record?
368
+ association.save(true)
369
+ self["#{association_class_primary_key_name}"] = association.id
370
+ association.send(:construct_sql)
371
+ end
372
+ EOF
373
+ end
374
+
375
+ if options[:counter_cache]
376
+ module_eval(
377
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
378
+ " unless #{association_name}.nil?'"
379
+ )
353
380
 
354
- module_eval(
355
- "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
356
- " if has_#{association_name}?'"
357
- )
358
- end
381
+ module_eval(
382
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
383
+ " unless #{association_name}.nil?'"
384
+ )
359
385
  end
360
386
 
387
+ # deprecated api
388
+ deprecated_has_association_method(association_name)
389
+ deprecated_association_comparison_method(association_name, association_class_name)
390
+ end
391
+
361
392
  # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
362
393
  # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
363
394
  # will give the default join table name of "developers_projects" because "D" outranks "P".
@@ -371,7 +402,7 @@ module ActiveRecord
371
402
  #
372
403
  # Adds the following methods for retrieval and query.
373
404
  # +collection+ is replaced with the symbol passed as the first argument, so
374
- # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
405
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +categories.empty?+.
375
406
  # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
376
407
  # An empty array is returned if none is found.
377
408
  # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
@@ -385,10 +416,13 @@ module ActiveRecord
385
416
  # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
386
417
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
387
418
  # * <tt>collection.size</tt> - returns the number of associated objects.
419
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
420
+ # meets the condition that it has to be associated with this object.
388
421
  #
389
422
  # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
390
423
  # * <tt>Developer#projects</tt>
391
424
  # * <tt>Developer#projects<<</tt>
425
+ # * <tt>Developer#projects.push_with_attributes</tt>
392
426
  # * <tt>Developer#projects.delete</tt>
393
427
  # * <tt>Developer#projects.clear</tt>
394
428
  # * <tt>Developer#projects.empty?</tt>
@@ -431,23 +465,13 @@ module ActiveRecord
431
465
 
432
466
  require_association_class(association_class_name)
433
467
 
434
- join_table = options[:join_table] ||
435
- join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
436
-
437
- define_method(association_name) do |*params|
438
- force_reload = params.first unless params.empty?
439
- association = instance_variable_get("@#{association_name}")
440
- if association.nil?
441
- association = HasAndBelongsToManyAssociation.new(self,
442
- association_name, association_class_name,
443
- association_class_primary_key_name, join_table, options)
444
- instance_variable_set("@#{association_name}", association)
445
- end
446
- association.reload if force_reload
447
- association
448
- end
468
+ options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
469
+
470
+ add_multiple_associated_save_callbacks(association_name)
471
+
472
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
449
473
 
450
- before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
474
+ before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
451
475
  module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
452
476
 
453
477
  # deprecated api
@@ -487,94 +511,70 @@ module ActiveRecord
487
511
  return association_id.id2name, association_class_name, primary_key_name
488
512
  end
489
513
 
490
- def association_comparison_method(association_name, association_class_name)
491
- module_eval <<-"end_eval", __FILE__, __LINE__
492
- def #{association_name}?(comparison_object, force_reload = false)
493
- if comparison_object.kind_of?(#{association_class_name})
494
- #{association_name}(force_reload) == comparison_object
495
- else
496
- raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
497
- end
514
+ def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
515
+ define_method(association_name) do |*params|
516
+ force_reload = params.first unless params.empty?
517
+ association = instance_variable_get("@#{association_name}")
518
+ unless association.respond_to?(:loaded?)
519
+ association = association_proxy_class.new(self,
520
+ association_name, association_class_name,
521
+ association_class_primary_key_name, options)
522
+ instance_variable_set("@#{association_name}", association)
498
523
  end
499
- end_eval
500
- end
524
+ association.reload if force_reload
525
+ association
526
+ end
501
527
 
502
- def association_reader_method(association_name, association_finder)
503
- module_eval <<-"end_eval", __FILE__, __LINE__
504
- def #{association_name}(force_reload = false)
505
- if @#{association_name}.nil? || force_reload
506
- begin
507
- @#{association_name} = #{association_finder}
508
- rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
509
- nil
510
- end
511
- end
512
-
513
- return @#{association_name}
528
+ define_method("#{association_name}=") do |new_value|
529
+ association = instance_variable_get("@#{association_name}")
530
+ unless association.respond_to?(:loaded?)
531
+ association = association_proxy_class.new(self,
532
+ association_name, association_class_name,
533
+ association_class_primary_key_name, options)
534
+ instance_variable_set("@#{association_name}", association)
514
535
  end
515
- end_eval
536
+ association.replace(new_value)
537
+ association
538
+ end
516
539
  end
517
540
 
518
- def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
519
- module_eval <<-"end_eval", __FILE__, __LINE__
520
- def #{association_name}=(association)
521
- if association.nil?
522
- @#{association_name}.#{class_primary_key_name} = nil
523
- @#{association_name}.save(false)
524
- @#{association_name} = nil
525
- else
526
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
527
- association.#{class_primary_key_name} = id
528
- association.save(false)
529
- @#{association_name} = association
530
- end
531
- end
532
- end_eval
541
+ def require_association_class(class_name)
542
+ require_association(Inflector.underscore(class_name)) if class_name
533
543
  end
534
544
 
535
- def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
536
- module_eval <<-"end_eval", __FILE__, __LINE__
537
- def #{association_name}=(association)
538
- if association.nil?
539
- @#{association_name} = self.#{association_class_primary_key_name} = nil
540
- else
541
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
542
- @#{association_name} = association
543
- self.#{association_class_primary_key_name} = association.id
545
+ def add_multiple_associated_save_callbacks(association_name)
546
+ module_eval do
547
+ before_save <<-end_eval
548
+ @new_record_before_save = new_record?
549
+ association = instance_variable_get("@#{association_name}")
550
+ if association.respond_to?(:loaded?)
551
+ if new_record?
552
+ records_to_save = association
553
+ else
554
+ records_to_save = association.select{ |record| record.new_record? }
555
+ end
556
+ records_to_save.inject(true) do |result,record|
557
+ result &&= record.valid?
558
+ end
544
559
  end
545
- end
546
- end_eval
547
- end
548
-
549
- def has_association_method(association_name)
550
- module_eval <<-"end_eval", __FILE__, __LINE__
551
- def has_#{association_name}?(force_reload = false)
552
- !#{association_name}(force_reload).nil?
553
- end
554
- end_eval
555
- end
556
-
557
- def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
558
- module_eval <<-"end_eval", __FILE__, __LINE__
559
- def #{method_prefix + collection_name}(attributes = {})
560
- association = #{collection_class_name}.new
561
- association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
562
- association
563
- end
564
- end_eval
565
- end
566
-
567
- def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
568
- module_eval <<-"end_eval", __FILE__, __LINE__
569
- def #{method_prefix + collection_name}(attributes = nil)
570
- #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
571
- end
572
- end_eval
573
- end
560
+ end_eval
561
+ end
574
562
 
575
- def require_association_class(class_name)
576
- require_association(Inflector.underscore(class_name)) if class_name
563
+ module_eval do
564
+ after_save <<-end_eval
565
+ association = instance_variable_get("@#{association_name}")
566
+ if association.respond_to?(:loaded?)
567
+ if @new_record_before_save
568
+ records_to_save = association
569
+ else
570
+ records_to_save = association.select{ |record| record.new_record? }
571
+ end
572
+ records_to_save.each{ |record| association.send(:insert_record, record) }
573
+ association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
574
+ end
575
+ end_eval
576
+ end
577
577
  end
578
578
  end
579
579
  end
580
- end
580
+ end