activerecord 1.0.0 → 1.1.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 +102 -1
- data/dev-utils/eval_debugger.rb +12 -7
- data/lib/active_record.rb +2 -0
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/associations.rb +74 -53
- data/lib/active_record/associations.rb.orig +555 -0
- data/lib/active_record/associations/association_collection.rb +74 -15
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +86 -25
- data/lib/active_record/associations/has_many_association.rb +48 -50
- data/lib/active_record/base.rb +56 -24
- data/lib/active_record/connection_adapters/abstract_adapter.rb +46 -3
- data/lib/active_record/connection_adapters/mysql_adapter.rb +15 -15
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +128 -135
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +76 -78
- data/lib/active_record/deprecated_associations.rb +1 -1
- data/lib/active_record/fixtures.rb +137 -54
- data/lib/active_record/observer.rb +1 -1
- data/lib/active_record/support/inflector.rb +8 -0
- data/lib/active_record/transactions.rb +31 -14
- data/rakefile +13 -5
- data/test/abstract_unit.rb +7 -1
- data/test/associations_test.rb +99 -27
- data/test/base_test.rb +15 -1
- data/test/connections/native_sqlite/connection.rb +24 -14
- data/test/deprecated_associations_test.rb +3 -4
- data/test/deprecated_associations_test.rb.orig +334 -0
- data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
- data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
- data/test/fixtures/bad_fixtures/blank_line +3 -0
- data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
- data/test/fixtures/bad_fixtures/missing_value +1 -0
- data/test/fixtures/company_in_module.rb +15 -1
- data/test/fixtures/db_definitions/mysql.sql +2 -1
- data/test/fixtures/db_definitions/postgresql.sql +2 -1
- data/test/fixtures/db_definitions/sqlite.sql +2 -1
- data/test/fixtures/developers_projects/david_action_controller +2 -1
- data/test/fixtures/developers_projects/david_active_record +2 -1
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/project.rb +2 -1
- data/test/fixtures/projects/action_controller +1 -1
- data/test/fixtures/topics/second +1 -1
- data/test/fixtures_test.rb +63 -4
- data/test/inflector_test.rb +17 -0
- data/test/modules_test.rb +8 -0
- data/test/transactions_test.rb +16 -4
- metadata +10 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,104 @@
|
|
1
|
+
*1.1.0* (34)
|
2
|
+
|
3
|
+
* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically
|
4
|
+
instantiated in instance variables relating to their names using the following style:
|
5
|
+
|
6
|
+
class FixturesTest < Test::Unit::TestCase
|
7
|
+
fixtures :developers # you can add more with comma separation
|
8
|
+
|
9
|
+
def test_developers
|
10
|
+
assert_equal 3, @developers.size # the container for all the fixtures is automatically set
|
11
|
+
assert_kind_of Developer, @david # works like @developers["david"].find
|
12
|
+
assert_equal "David Heinemeier Hansson", @david.name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
* Added HasAndBelongsToManyAssociation#push_with_attributes(object, join_attributes) that can create associations in the join table with additional
|
17
|
+
attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association
|
18
|
+
between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back
|
19
|
+
approach:
|
20
|
+
|
21
|
+
post.categories.push_with_attributes(category, :added_on => Date.today)
|
22
|
+
post.categories.first.added_on # => Date.today
|
23
|
+
|
24
|
+
NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does!
|
25
|
+
|
26
|
+
* Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations [bitsweat]
|
27
|
+
|
28
|
+
* Fixed that database passwords couldn't be all numeric [bitsweat]
|
29
|
+
|
30
|
+
* Fixed that calling id would create the instance variable for new_records preventing them from being saved correctly [bitsweat]
|
31
|
+
|
32
|
+
* Added sanitization feature to HasManyAssociation#find_all so it works just like Base.find_all [Sam Stephenson/bitsweat]
|
33
|
+
|
34
|
+
* Added that you can pass overlapping ids to find without getting duplicated records back [bitsweat]
|
35
|
+
|
36
|
+
* Added that Base.benchmark returns the result of the block [bitsweat]
|
37
|
+
|
38
|
+
* Fixed problem with unit tests on Windows with SQLite [paterno]
|
39
|
+
|
40
|
+
* Fixed that quotes would break regular non-yaml fixtures [Dmitry Sabanin/daft]
|
41
|
+
|
42
|
+
* Fixed fixtures on windows with line endings cause problems under unix / mac [Tobias Luetke]
|
43
|
+
|
44
|
+
* Added HasAndBelongsToManyAssociation#find(id) that'll search inside the collection and find the object or record with that id
|
45
|
+
|
46
|
+
* Added :conditions option to has_and_belongs_to_many that works just like the one on all the other associations
|
47
|
+
|
48
|
+
* Added AssociationCollection#clear to remove all associations from has_many and has_and_belongs_to_many associations without destroying the records [geech]
|
49
|
+
|
50
|
+
* Added type-checking and remove in 1-instead-of-N sql statements to AssociationCollection#delete [geech]
|
51
|
+
|
52
|
+
* Added a return of self to AssociationCollection#<< so appending can be chained, like project << Milestone.create << Milestone.create [geech]
|
53
|
+
|
54
|
+
* Added Base#hash and Base#eql? which means that all of the equality using features of array and other containers now works:
|
55
|
+
|
56
|
+
[ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
57
|
+
|
58
|
+
* Added :uniq as an option to has_and_belongs_to_many which will automatically ensure that AssociateCollection#uniq is called
|
59
|
+
before pulling records out of the association. This is especially useful for three-way (and above) has_and_belongs_to_many associations.
|
60
|
+
|
61
|
+
* Added AssociateCollection#uniq which is especially useful for has_and_belongs_to_many associations that can include duplicates,
|
62
|
+
which is common on associations that also use metadata. Usage: post.categories.uniq
|
63
|
+
|
64
|
+
* Fixed respond_to? to use a subclass specific hash instead of an Active Record-wide one
|
65
|
+
|
66
|
+
* Fixed has_and_belongs_to_many to treat associations between classes in modules properly [Florian Weber]
|
67
|
+
|
68
|
+
* Added a NoMethod exception to be raised when query and writer methods are called for attributes that doesn't exist [geech]
|
69
|
+
|
70
|
+
* Added a more robust version of Fixtures that throws meaningful errors when on formatting issues [geech]
|
71
|
+
|
72
|
+
* Added Base#transaction as a compliment to Base.transaction for prettier use in instance methods [geech]
|
73
|
+
|
74
|
+
* Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash [geech]
|
75
|
+
|
76
|
+
* Added that any additional fields added to the join table in a has_and_belongs_to_many association
|
77
|
+
will be placed as attributes when pulling records out through has_and_belongs_to_many associations.
|
78
|
+
This is helpful when have information about the association itself that you want available on retrival.
|
79
|
+
|
80
|
+
* Added better loading exception catching and RubyGems retries to the database adapters [alexeyv]
|
81
|
+
|
82
|
+
* Fixed bug with per-model transactions [daniel]
|
83
|
+
|
84
|
+
* Fixed Base#transaction so that it returns the result of the last expression in the transaction block [alexeyv]
|
85
|
+
|
86
|
+
* Added Fixture#find to find the record corresponding to the fixture id. The record
|
87
|
+
class name is guessed by using Inflector#classify (also new) on the fixture directory name.
|
88
|
+
|
89
|
+
Before: Document.find(@documents["first"]["id"])
|
90
|
+
After : @documents["first"].find
|
91
|
+
|
92
|
+
* Fixed that the table name part of column names ("TABLE.COLUMN") wasn't removed properly [Andreas Schwarz]
|
93
|
+
|
94
|
+
* Fixed a bug with Base#size when a finder_sql was used that didn't capitalize SELECT and FROM [geech]
|
95
|
+
|
96
|
+
* Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete
|
97
|
+
adapters for a call to the dbm. [Andreas Schwarz]
|
98
|
+
|
99
|
+
* Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR.
|
100
|
+
|
101
|
+
|
1
102
|
*1.0.0 (35)*
|
2
103
|
|
3
104
|
* Added OO-style associations methods [Florian Weber]. Examples:
|
@@ -45,7 +146,7 @@
|
|
45
146
|
log { result = ... }
|
46
147
|
result.map { ... }
|
47
148
|
|
48
|
-
* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna
|
149
|
+
* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna Lissa Cruz]
|
49
150
|
|
50
151
|
* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema,
|
51
152
|
person.respond_to? "name" will return true.
|
data/dev-utils/eval_debugger.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
-
# Require
|
2
|
-
# All the additions are reported to $stderr just by requiring this file.
|
1
|
+
# Require this file to see the methods Active Record generates as they are added.
|
3
2
|
class Module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
alias :old_module_eval :module_eval
|
4
|
+
def module_eval(*args, &block)
|
5
|
+
if args[0]
|
6
|
+
puts "----"
|
7
|
+
print "module_eval in #{self.name}"
|
8
|
+
print ": file #{args[1]}" if args[1]
|
9
|
+
print " on line #{args[2]}" if args[2]
|
10
|
+
puts "\n#{args[0]}"
|
11
|
+
end
|
12
|
+
old_module_eval(*args, &block)
|
13
|
+
end
|
9
14
|
end
|
data/lib/active_record.rb
CHANGED
@@ -21,9 +21,11 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
|
24
|
+
|
24
25
|
$:.unshift(File.dirname(__FILE__))
|
25
26
|
|
26
27
|
require 'active_record/support/clean_logger'
|
28
|
+
# require 'active_record/support/array_ext'
|
27
29
|
|
28
30
|
require 'active_record/base'
|
29
31
|
require 'active_record/observer'
|
@@ -22,9 +22,9 @@ module ActiveRecord
|
|
22
22
|
# has_and_belongs_to_many :categories
|
23
23
|
# end
|
24
24
|
#
|
25
|
-
# The project class now has the following methods to ease the
|
25
|
+
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
|
26
26
|
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
|
27
|
-
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#
|
27
|
+
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
|
28
28
|
# <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
|
29
29
|
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
|
30
30
|
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
|
@@ -119,10 +119,11 @@ module ActiveRecord
|
|
119
119
|
# +collection+ is replaced with the symbol passed as the first argument, so
|
120
120
|
# <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
|
121
121
|
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
122
|
-
# An empty array is returned if none
|
123
|
-
# * <tt>collection<<(object)</tt> - adds
|
124
|
-
# * <tt>collection.delete(object)</tt> - removes the
|
125
|
-
# * <tt
|
122
|
+
# An empty array is returned if none are found.
|
123
|
+
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
124
|
+
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
|
125
|
+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
126
|
+
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
126
127
|
# * <tt>collection.size</tt> - returns the number of associated objects.
|
127
128
|
# * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
|
128
129
|
# meets the condition that it has to be associated with this object.
|
@@ -137,13 +138,14 @@ module ActiveRecord
|
|
137
138
|
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
|
138
139
|
# * <tt>Firm#clients<<</tt>
|
139
140
|
# * <tt>Firm#clients.delete</tt>
|
140
|
-
# * <tt
|
141
|
+
# * <tt>Firm#clients.clear</tt>
|
142
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
141
143
|
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
142
144
|
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
|
143
145
|
# * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
|
144
146
|
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
145
147
|
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
|
146
|
-
# The declaration can also include an options hash to specialize the
|
148
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
147
149
|
#
|
148
150
|
# Options are:
|
149
151
|
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
@@ -156,10 +158,12 @@ module ActiveRecord
|
|
156
158
|
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
|
157
159
|
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
|
158
160
|
# as the default foreign_key.
|
159
|
-
# * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object
|
161
|
+
# * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
|
162
|
+
# May not be set if :exclusively_dependent is also set.
|
160
163
|
# * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
|
161
164
|
# before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
|
162
165
|
# clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
|
166
|
+
# May not be set if :dependent is also set.
|
163
167
|
# * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
164
168
|
# associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
165
169
|
#
|
@@ -177,25 +181,26 @@ module ActiveRecord
|
|
177
181
|
association_name, association_class_name, association_class_primary_key_name =
|
178
182
|
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
179
183
|
|
180
|
-
if options[:dependent]
|
184
|
+
if options[:dependent] and options[:exclusively_dependent]
|
185
|
+
raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
|
186
|
+
elsif options[:dependent]
|
181
187
|
module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
|
188
|
+
elsif options[:exclusively_dependent]
|
189
|
+
module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
|
182
190
|
end
|
183
191
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
"#{association_class_primary_key_name}", #{options.inspect})
|
193
|
-
end
|
194
|
-
@#{association_name}.reload if force_reload
|
195
|
-
|
196
|
-
return @#{association_name}
|
192
|
+
define_method(association_name) do |*params|
|
193
|
+
force_reload = params.first unless params.empty?
|
194
|
+
association = instance_variable_get("@#{association_name}")
|
195
|
+
if association.nil?
|
196
|
+
association = HasManyAssociation.new(self,
|
197
|
+
association_name, association_class_name,
|
198
|
+
association_class_primary_key_name, options)
|
199
|
+
instance_variable_set("@#{association_name}", association)
|
197
200
|
end
|
198
|
-
|
201
|
+
association.reload if force_reload
|
202
|
+
association
|
203
|
+
end
|
199
204
|
|
200
205
|
# deprecated api
|
201
206
|
deprecated_collection_count_method(association_name)
|
@@ -216,7 +221,7 @@ module ActiveRecord
|
|
216
221
|
# and saves the associate object.
|
217
222
|
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
218
223
|
# same id as the associated object.
|
219
|
-
# * <tt
|
224
|
+
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
220
225
|
# * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
221
226
|
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
|
222
227
|
# * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
@@ -226,10 +231,10 @@ module ActiveRecord
|
|
226
231
|
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
|
227
232
|
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
228
233
|
# * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
|
229
|
-
# * <tt
|
234
|
+
# * <tt>Account#beneficiary.nil?</tt>
|
230
235
|
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
231
236
|
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
232
|
-
# The declaration can also include an options hash to specialize the
|
237
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
233
238
|
#
|
234
239
|
# Options are:
|
235
240
|
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
@@ -269,14 +274,14 @@ module ActiveRecord
|
|
269
274
|
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
270
275
|
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
271
276
|
# same id as the associated object.
|
272
|
-
# * <tt>association.nil?</tt> - returns true if there
|
277
|
+
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
273
278
|
#
|
274
279
|
# Example: An Post class declares <tt>has_one :author</tt>, which will add:
|
275
280
|
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
276
281
|
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
277
282
|
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
278
|
-
# * <tt
|
279
|
-
# The declaration can also include an options hash to specialize the
|
283
|
+
# * <tt>Post#author.nil?</tt>
|
284
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
280
285
|
#
|
281
286
|
# Options are:
|
282
287
|
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
@@ -341,25 +346,37 @@ module ActiveRecord
|
|
341
346
|
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
|
342
347
|
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
|
343
348
|
# will give the default join table name of "developers_projects" because "D" outranks "P".
|
349
|
+
#
|
350
|
+
# Any additional fields added to the join table will be placed as attributes when pulling records out through
|
351
|
+
# has_and_belongs_to_many associations. This is helpful when have information about the association itself
|
352
|
+
# that you want available on retrival.
|
353
|
+
#
|
344
354
|
# Adds the following methods for retrival and query.
|
345
355
|
# +collection+ is replaced with the symbol passed as the first argument, so
|
346
356
|
# <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
|
347
357
|
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
348
358
|
# An empty array is returned if none is found.
|
349
|
-
# * <tt
|
359
|
+
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
|
360
|
+
# (collection.push and collection.concat are aliases to this method).
|
361
|
+
# * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
|
362
|
+
# also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
|
363
|
+
# attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
|
364
|
+
# (collection.concat_with_attributes is an alias to this method).
|
365
|
+
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
|
366
|
+
# This does not destroy the objects.
|
367
|
+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
368
|
+
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
350
369
|
# * <tt>collection.size</tt> - returns the number of associated objects.
|
351
|
-
# * <tt>collection<<(object)</tt> - adds an association between this object and the object given as argument. Multiple associations
|
352
|
-
# can be created by passing an array of objects instead.
|
353
|
-
# * <tt>collection.delete(object)</tt> - removes the association between this object and the object given as
|
354
|
-
# argument. Multiple associations can be removed by passing an array of objects instead.
|
355
370
|
#
|
356
371
|
# Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
357
372
|
# * <tt>Developer#projects</tt>
|
358
|
-
# * <tt>!Developer#projects.empty?</tt>
|
359
|
-
# * <tt>Developer#projects.size</tt>
|
360
373
|
# * <tt>Developer#projects<<</tt>
|
361
374
|
# * <tt>Developer#projects.delete</tt>
|
362
|
-
#
|
375
|
+
# * <tt>Developer#projects.clear</tt>
|
376
|
+
# * <tt>Developer#projects.empty?</tt>
|
377
|
+
# * <tt>Developer#projects.size</tt>
|
378
|
+
# * <tt>Developer#projects.find(id)</tt>
|
379
|
+
# The declaration may include an options hash to specialize the behavior of the association.
|
363
380
|
#
|
364
381
|
# Options are:
|
365
382
|
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
@@ -374,7 +391,10 @@ module ActiveRecord
|
|
374
391
|
# * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
|
375
392
|
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
|
376
393
|
# that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
|
377
|
-
# * <tt>:
|
394
|
+
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
|
395
|
+
# sql fragment, such as "authorized = 1".
|
396
|
+
# * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
|
397
|
+
# * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
|
378
398
|
# * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
|
379
399
|
# * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
|
380
400
|
# classes with a manual one
|
@@ -386,8 +406,8 @@ module ActiveRecord
|
|
386
406
|
# has_and_belongs_to_many :nations, :class_name => "Country"
|
387
407
|
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
|
388
408
|
def has_and_belongs_to_many(association_id, options = {})
|
389
|
-
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key,
|
390
|
-
:join_table, :finder_sql, :delete_sql, :insert_sql, :order ], options.keys)
|
409
|
+
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
|
410
|
+
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
|
391
411
|
association_name, association_class_name, association_class_primary_key_name =
|
392
412
|
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
393
413
|
|
@@ -395,19 +415,20 @@ module ActiveRecord
|
|
395
415
|
join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
|
396
416
|
|
397
417
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
return @#{association_name}
|
418
|
+
define_method(association_name) do |*params|
|
419
|
+
force_reload = params.first unless params.empty?
|
420
|
+
association = instance_variable_get("@#{association_name}")
|
421
|
+
if association.nil?
|
422
|
+
association = HasAndBelongsToManyAssociation.new(self,
|
423
|
+
association_name, association_class_name,
|
424
|
+
association_class_primary_key_name, join_table, options)
|
425
|
+
instance_variable_set("@#{association_name}", association)
|
407
426
|
end
|
408
|
-
|
427
|
+
association.reload if force_reload
|
428
|
+
association
|
429
|
+
end
|
409
430
|
|
410
|
-
before_destroy_sql = "DELETE FROM #{join_table} WHERE #{
|
431
|
+
before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
|
411
432
|
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
|
412
433
|
|
413
434
|
# deprecated api
|
@@ -533,4 +554,4 @@ module ActiveRecord
|
|
533
554
|
end
|
534
555
|
end
|
535
556
|
end
|
536
|
-
end
|
557
|
+
end
|
@@ -0,0 +1,555 @@
|
|
1
|
+
require 'active_record/associations/association_collection'
|
2
|
+
require 'active_record/associations/has_many_association'
|
3
|
+
require 'active_record/associations/has_and_belongs_to_many_association'
|
4
|
+
require 'active_record/deprecated_associations'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Associations # :nodoc:
|
8
|
+
def self.append_features(base)
|
9
|
+
super
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
|
14
|
+
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
|
15
|
+
# specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
|
16
|
+
# methods. Example:
|
17
|
+
#
|
18
|
+
# class Project < ActiveRecord::Base
|
19
|
+
# belongs_to :portfolio
|
20
|
+
# has_one :project_manager
|
21
|
+
# has_many :milestones
|
22
|
+
# has_and_belongs_to_many :categories
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
|
26
|
+
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
|
27
|
+
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
|
28
|
+
# <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
|
29
|
+
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
|
30
|
+
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
|
31
|
+
# <tt>Project#milestones.build, Project#milestones.create</tt>
|
32
|
+
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
|
33
|
+
# <tt>Project#categories.delete(category1)</tt>
|
34
|
+
#
|
35
|
+
# == Example
|
36
|
+
#
|
37
|
+
# link:../examples/associations.png
|
38
|
+
#
|
39
|
+
# == Is it belongs_to or has_one?
|
40
|
+
#
|
41
|
+
# Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
|
42
|
+
# saying belongs_to. Example:
|
43
|
+
#
|
44
|
+
# class Post < ActiveRecord::Base
|
45
|
+
# has_one :author
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# class Author < ActiveRecord::Base
|
49
|
+
# belongs_to :post
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# The tables for these classes could look something like:
|
53
|
+
#
|
54
|
+
# CREATE TABLE posts (
|
55
|
+
# id int(11) NOT NULL auto_increment,
|
56
|
+
# title varchar default NULL,
|
57
|
+
# PRIMARY KEY (id)
|
58
|
+
# )
|
59
|
+
#
|
60
|
+
# CREATE TABLE authors (
|
61
|
+
# id int(11) NOT NULL auto_increment,
|
62
|
+
# post_id int(11) default NULL,
|
63
|
+
# name varchar default NULL,
|
64
|
+
# PRIMARY KEY (id)
|
65
|
+
# )
|
66
|
+
#
|
67
|
+
# == Caching
|
68
|
+
#
|
69
|
+
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
70
|
+
# instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
|
71
|
+
# worrying too much about performance at the first go. Example:
|
72
|
+
#
|
73
|
+
# project.milestones # fetches milestones from the database
|
74
|
+
# project.milestones.size # uses the milestone cache
|
75
|
+
# project.milestones.empty? # uses the milestone cache
|
76
|
+
# project.milestones(true).size # fetches milestones from the database
|
77
|
+
# project.milestones # uses the milestone cache
|
78
|
+
#
|
79
|
+
# == Modules
|
80
|
+
#
|
81
|
+
# By default, associations will look for objects within the current module scope. Consider:
|
82
|
+
#
|
83
|
+
# module MyApplication
|
84
|
+
# module Business
|
85
|
+
# class Firm < ActiveRecord::Base
|
86
|
+
# has_many :clients
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# class Company < ActiveRecord::Base; end
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
|
94
|
+
# with a class in another module scope this can be done by specifying the complete class name, such as:
|
95
|
+
#
|
96
|
+
# module MyApplication
|
97
|
+
# module Business
|
98
|
+
# class Firm < ActiveRecord::Base; end
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# module Billing
|
102
|
+
# class Account < ActiveRecord::Base
|
103
|
+
# belongs_to :firm, :class_name => "MyApplication::Business::Firm"
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# == Type safety with ActiveRecord::AssociationTypeMismatch
|
109
|
+
#
|
110
|
+
# If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
|
111
|
+
# get a ActiveRecord::AssociationTypeMismatch.
|
112
|
+
#
|
113
|
+
# == Options
|
114
|
+
#
|
115
|
+
# All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
|
116
|
+
# possible.
|
117
|
+
module ClassMethods
|
118
|
+
# Adds the following methods for retrival and query of collections of associated objects.
|
119
|
+
# +collection+ is replaced with the symbol passed as the first argument, so
|
120
|
+
# <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
|
121
|
+
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
122
|
+
# An empty array is returned if none are found.
|
123
|
+
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
124
|
+
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
|
125
|
+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
126
|
+
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
127
|
+
# * <tt>collection.size</tt> - returns the number of associated objects.
|
128
|
+
# * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
|
129
|
+
# meets the condition that it has to be associated with this object.
|
130
|
+
# * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
|
131
|
+
# criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
|
132
|
+
# * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
|
133
|
+
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
|
134
|
+
# * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
|
135
|
+
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
|
136
|
+
#
|
137
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
138
|
+
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
|
139
|
+
# * <tt>Firm#clients<<</tt>
|
140
|
+
# * <tt>Firm#clients.delete</tt>
|
141
|
+
# * <tt>Firm#clients.clear</tt>
|
142
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
143
|
+
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
144
|
+
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
|
145
|
+
# * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
|
146
|
+
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
147
|
+
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
|
148
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
149
|
+
#
|
150
|
+
# Options are:
|
151
|
+
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
152
|
+
# from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
|
153
|
+
# if the real class name is +SpecialProduct+, you'll have to specify it with this option.
|
154
|
+
# * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
|
155
|
+
# sql fragment, such as "price > 5 AND name LIKE 'B%'".
|
156
|
+
# * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
|
157
|
+
# such as "last_name, first_name DESC"
|
158
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
|
159
|
+
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
|
160
|
+
# as the default foreign_key.
|
161
|
+
# * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object
|
162
|
+
# * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
|
163
|
+
# before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
|
164
|
+
# clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
|
165
|
+
# * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
166
|
+
# associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
167
|
+
#
|
168
|
+
# Option examples:
|
169
|
+
# has_many :comments, :order => "posted_on"
|
170
|
+
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
|
171
|
+
# has_many :tracks, :order => "position", :dependent => true
|
172
|
+
# has_many :subscribers, :class_name => "Person", :finder_sql =>
|
173
|
+
# 'SELECT DISTINCT people.* ' +
|
174
|
+
# 'FROM people p, post_subscriptions ps ' +
|
175
|
+
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
176
|
+
# 'ORDER BY p.first_name'
|
177
|
+
def has_many(association_id, options = {})
|
178
|
+
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
|
179
|
+
association_name, association_class_name, association_class_primary_key_name =
|
180
|
+
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
181
|
+
|
182
|
+
if options[:dependent]
|
183
|
+
module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
|
184
|
+
end
|
185
|
+
|
186
|
+
if options[:exclusively_dependent]
|
187
|
+
module_eval "before_destroy Proc.new{ |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
|
188
|
+
end
|
189
|
+
|
190
|
+
define_method(association_name) do |*params|
|
191
|
+
force_reload = params.first unless params.empty?
|
192
|
+
association = instance_variable_get("@#{association_name}")
|
193
|
+
if association.nil?
|
194
|
+
association = HasManyAssociation.new(self,
|
195
|
+
association_name, association_class_name,
|
196
|
+
association_class_primary_key_name, options)
|
197
|
+
instance_variable_set("@#{association_name}", association)
|
198
|
+
end
|
199
|
+
association.reload if force_reload
|
200
|
+
association
|
201
|
+
end
|
202
|
+
|
203
|
+
# deprecated api
|
204
|
+
deprecated_collection_count_method(association_name)
|
205
|
+
deprecated_add_association_relation(association_name)
|
206
|
+
deprecated_remove_association_relation(association_name)
|
207
|
+
deprecated_has_collection_method(association_name)
|
208
|
+
deprecated_find_in_collection_method(association_name)
|
209
|
+
deprecated_find_all_in_collection_method(association_name)
|
210
|
+
deprecated_create_method(association_name)
|
211
|
+
deprecated_build_method(association_name)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Adds the following methods for retrival and query of a single associated object.
|
215
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
216
|
+
# <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
|
217
|
+
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
|
218
|
+
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
|
219
|
+
# and saves the associate object.
|
220
|
+
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
221
|
+
# same id as the associated object.
|
222
|
+
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
223
|
+
# * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
224
|
+
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
|
225
|
+
# * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
226
|
+
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
|
227
|
+
#
|
228
|
+
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
229
|
+
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
|
230
|
+
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
231
|
+
# * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
|
232
|
+
# * <tt>Account#beneficiary.nil?</tt>
|
233
|
+
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
234
|
+
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
235
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
236
|
+
#
|
237
|
+
# Options are:
|
238
|
+
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
239
|
+
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
|
240
|
+
# if the real class name is +Person+, you'll have to specify it with this option.
|
241
|
+
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
|
242
|
+
# sql fragment, such as "rank = 5".
|
243
|
+
# * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
|
244
|
+
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
|
245
|
+
# * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
|
246
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
|
247
|
+
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
|
248
|
+
# as the default foreign_key.
|
249
|
+
#
|
250
|
+
# Option examples:
|
251
|
+
# has_one :credit_card, :dependent => true
|
252
|
+
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
|
253
|
+
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
254
|
+
def has_one(association_id, options = {})
|
255
|
+
options.merge!({ :remote => true })
|
256
|
+
belongs_to(association_id, options)
|
257
|
+
|
258
|
+
association_name, association_class_name, class_primary_key_name =
|
259
|
+
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
|
260
|
+
|
261
|
+
has_one_writer_method(association_name, association_class_name, class_primary_key_name)
|
262
|
+
build_method("build_", association_name, association_class_name, class_primary_key_name)
|
263
|
+
create_method("create_", association_name, association_class_name, class_primary_key_name)
|
264
|
+
|
265
|
+
module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
|
266
|
+
end
|
267
|
+
|
268
|
+
# Adds the following methods for retrival and query for a single associated object that this object holds an id to.
|
269
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
270
|
+
# <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
|
271
|
+
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
|
272
|
+
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
273
|
+
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
274
|
+
# same id as the associated object.
|
275
|
+
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
276
|
+
#
|
277
|
+
# Example: An Post class declares <tt>has_one :author</tt>, which will add:
|
278
|
+
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
279
|
+
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
280
|
+
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
281
|
+
# * <tt>Post#author.nil?</tt>
|
282
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
283
|
+
#
|
284
|
+
# Options are:
|
285
|
+
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
286
|
+
# from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
|
287
|
+
# if the real class name is +Person+, you'll have to specify it with this option.
|
288
|
+
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
|
289
|
+
# sql fragment, such as "authorized = 1".
|
290
|
+
# * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
|
291
|
+
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
|
292
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
|
293
|
+
# of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
|
294
|
+
# +Boss+ class will use "boss_id" as the default foreign_key.
|
295
|
+
# * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
|
296
|
+
# and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
|
297
|
+
# destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
|
298
|
+
# is used on the associate class (such as a Post class).
|
299
|
+
#
|
300
|
+
# Option examples:
|
301
|
+
# belongs_to :firm, :foreign_key => "client_of"
|
302
|
+
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
303
|
+
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
304
|
+
# :conditions => 'discounts > #{payments_count}'
|
305
|
+
def belongs_to(association_id, options = {})
|
306
|
+
validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
|
307
|
+
|
308
|
+
association_name, association_class_name, class_primary_key_name =
|
309
|
+
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
|
310
|
+
|
311
|
+
association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
|
312
|
+
|
313
|
+
if options[:remote]
|
314
|
+
association_finder = <<-"end_eval"
|
315
|
+
#{association_class_name}.find_first(
|
316
|
+
"#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
|
317
|
+
#{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
|
318
|
+
)
|
319
|
+
end_eval
|
320
|
+
else
|
321
|
+
association_finder = options[:conditions] ?
|
322
|
+
"#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
|
323
|
+
"#{association_class_name}.find(#{association_class_primary_key_name})"
|
324
|
+
end
|
325
|
+
|
326
|
+
has_association_method(association_name)
|
327
|
+
association_reader_method(association_name, association_finder)
|
328
|
+
belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
|
329
|
+
association_comparison_method(association_name, association_class_name)
|
330
|
+
|
331
|
+
if options[:counter_cache]
|
332
|
+
module_eval(
|
333
|
+
"after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
|
334
|
+
" if has_#{association_name}?'"
|
335
|
+
)
|
336
|
+
|
337
|
+
module_eval(
|
338
|
+
"before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
|
339
|
+
" if has_#{association_name}?'"
|
340
|
+
)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
|
345
|
+
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
|
346
|
+
# will give the default join table name of "developers_projects" because "D" outranks "P".
|
347
|
+
#
|
348
|
+
# Any additional fields added to the join table will be placed as attributes when pulling records out through
|
349
|
+
# has_and_belongs_to_many associations. This is helpful when have information about the association itself
|
350
|
+
# that you want available on retrival.
|
351
|
+
#
|
352
|
+
# Adds the following methods for retrival and query.
|
353
|
+
# +collection+ is replaced with the symbol passed as the first argument, so
|
354
|
+
# <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
|
355
|
+
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
356
|
+
# An empty array is returned if none is found.
|
357
|
+
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
|
358
|
+
# (collection.push and collection.concat are aliases to this method).
|
359
|
+
# * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
|
360
|
+
# also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
|
361
|
+
# attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
|
362
|
+
# (collection.concat_with_attributes is an alias to this method).
|
363
|
+
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
|
364
|
+
# This does not destroy the objects.
|
365
|
+
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
366
|
+
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
367
|
+
# * <tt>collection.size</tt> - returns the number of associated objects.
|
368
|
+
#
|
369
|
+
# Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
370
|
+
# * <tt>Developer#projects</tt>
|
371
|
+
# * <tt>Developer#projects<<</tt>
|
372
|
+
# * <tt>Developer#projects.delete</tt>
|
373
|
+
# * <tt>Developer#projects.clear</tt>
|
374
|
+
# * <tt>Developer#projects.empty?</tt>
|
375
|
+
# * <tt>Developer#projects.size</tt>
|
376
|
+
# * <tt>Developer#projects.find(id)</tt>
|
377
|
+
# The declaration may include an options hash to specialize the behavior of the association.
|
378
|
+
#
|
379
|
+
# Options are:
|
380
|
+
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
|
381
|
+
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
|
382
|
+
# +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
|
383
|
+
# * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
|
384
|
+
# WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
|
385
|
+
# has_and_belongs_to_many declaration in order to work.
|
386
|
+
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
|
387
|
+
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
|
388
|
+
# will use "person_id" as the default foreign_key.
|
389
|
+
# * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
|
390
|
+
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
|
391
|
+
# that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
|
392
|
+
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
|
393
|
+
# sql fragment, such as "authorized = 1".
|
394
|
+
# * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
|
395
|
+
# * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
|
396
|
+
# * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
|
397
|
+
# * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
|
398
|
+
# classes with a manual one
|
399
|
+
# * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
|
400
|
+
# with a manual one
|
401
|
+
#
|
402
|
+
# Option examples:
|
403
|
+
# has_and_belongs_to_many :projects
|
404
|
+
# has_and_belongs_to_many :nations, :class_name => "Country"
|
405
|
+
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
|
406
|
+
def has_and_belongs_to_many(association_id, options = {})
|
407
|
+
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
|
408
|
+
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
|
409
|
+
association_name, association_class_name, association_class_primary_key_name =
|
410
|
+
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
411
|
+
|
412
|
+
join_table = options[:join_table] ||
|
413
|
+
join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
|
414
|
+
|
415
|
+
|
416
|
+
define_method(association_name) do |*params|
|
417
|
+
force_reload = params.first unless params.empty?
|
418
|
+
association = instance_variable_get("@#{association_name}")
|
419
|
+
if association.nil?
|
420
|
+
association = HasAndBelongsToManyAssociation.new(self,
|
421
|
+
association_name, association_class_name,
|
422
|
+
association_class_primary_key_name, join_table, options)
|
423
|
+
instance_variable_set("@#{association_name}", association)
|
424
|
+
end
|
425
|
+
association.reload if force_reload
|
426
|
+
association
|
427
|
+
end
|
428
|
+
|
429
|
+
before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
|
430
|
+
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
|
431
|
+
|
432
|
+
# deprecated api
|
433
|
+
deprecated_collection_count_method(association_name)
|
434
|
+
deprecated_add_association_relation(association_name)
|
435
|
+
deprecated_remove_association_relation(association_name)
|
436
|
+
deprecated_has_collection_method(association_name)
|
437
|
+
end
|
438
|
+
|
439
|
+
private
|
440
|
+
# Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
|
441
|
+
def validate_options(valid_option_keys, supplied_option_keys)
|
442
|
+
unknown_option_keys = supplied_option_keys - valid_option_keys
|
443
|
+
raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
|
444
|
+
end
|
445
|
+
|
446
|
+
def join_table_name(first_table_name, second_table_name)
|
447
|
+
if first_table_name < second_table_name
|
448
|
+
join_table = "#{first_table_name}_#{second_table_name}"
|
449
|
+
else
|
450
|
+
join_table = "#{second_table_name}_#{first_table_name}"
|
451
|
+
end
|
452
|
+
|
453
|
+
table_name_prefix + join_table + table_name_suffix
|
454
|
+
end
|
455
|
+
|
456
|
+
def associate_identification(association_id, association_class_name, foreign_key, plural = true)
|
457
|
+
if association_class_name !~ /::/
|
458
|
+
association_class_name = type_name_with_module(
|
459
|
+
association_class_name ||
|
460
|
+
Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
|
461
|
+
)
|
462
|
+
end
|
463
|
+
|
464
|
+
primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
|
465
|
+
|
466
|
+
return association_id.id2name, association_class_name, primary_key_name
|
467
|
+
end
|
468
|
+
|
469
|
+
def association_comparison_method(association_name, association_class_name)
|
470
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
471
|
+
def #{association_name}?(comparison_object, force_reload = false)
|
472
|
+
if comparison_object.kind_of?(#{association_class_name})
|
473
|
+
#{association_name}(force_reload) == comparison_object
|
474
|
+
else
|
475
|
+
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end_eval
|
479
|
+
end
|
480
|
+
|
481
|
+
def association_reader_method(association_name, association_finder)
|
482
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
483
|
+
def #{association_name}(force_reload = false)
|
484
|
+
if @#{association_name}.nil? || force_reload
|
485
|
+
begin
|
486
|
+
@#{association_name} = #{association_finder}
|
487
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
|
488
|
+
nil
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
return @#{association_name}
|
493
|
+
end
|
494
|
+
end_eval
|
495
|
+
end
|
496
|
+
|
497
|
+
def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
|
498
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
499
|
+
def #{association_name}=(association)
|
500
|
+
if association.nil?
|
501
|
+
@#{association_name}.#{class_primary_key_name} = nil
|
502
|
+
@#{association_name}.save(false)
|
503
|
+
@#{association_name} = nil
|
504
|
+
else
|
505
|
+
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
|
506
|
+
association.#{class_primary_key_name} = id
|
507
|
+
association.save(false)
|
508
|
+
@#{association_name} = association
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end_eval
|
512
|
+
end
|
513
|
+
|
514
|
+
def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
|
515
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
516
|
+
def #{association_name}=(association)
|
517
|
+
if association.nil?
|
518
|
+
@#{association_name} = self.#{association_class_primary_key_name} = nil
|
519
|
+
else
|
520
|
+
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
|
521
|
+
@#{association_name} = association
|
522
|
+
self.#{association_class_primary_key_name} = association.id
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end_eval
|
526
|
+
end
|
527
|
+
|
528
|
+
def has_association_method(association_name)
|
529
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
530
|
+
def has_#{association_name}?(force_reload = false)
|
531
|
+
!#{association_name}(force_reload).nil?
|
532
|
+
end
|
533
|
+
end_eval
|
534
|
+
end
|
535
|
+
|
536
|
+
def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
537
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
538
|
+
def #{method_prefix + collection_name}(attributes = {})
|
539
|
+
association = #{collection_class_name}.new
|
540
|
+
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
|
541
|
+
association
|
542
|
+
end
|
543
|
+
end_eval
|
544
|
+
end
|
545
|
+
|
546
|
+
def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
547
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
548
|
+
def #{method_prefix + collection_name}(attributes = nil)
|
549
|
+
#{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
|
550
|
+
end
|
551
|
+
end_eval
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|