georgepalmer-couch_foo 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +113 -0
- data/VERSION.yml +4 -0
- data/lib/boolean.rb +3 -0
- data/lib/couch_foo/associations/association_collection.rb +346 -0
- data/lib/couch_foo/associations/association_proxy.rb +204 -0
- data/lib/couch_foo/associations/belongs_to_association.rb +57 -0
- data/lib/couch_foo/associations/belongs_to_polymorphic_association.rb +48 -0
- data/lib/couch_foo/associations/has_and_belongs_to_many_association.rb +111 -0
- data/lib/couch_foo/associations/has_many_association.rb +97 -0
- data/lib/couch_foo/associations/has_one_association.rb +95 -0
- data/lib/couch_foo/associations.rb +1118 -0
- data/lib/couch_foo/attribute_methods.rb +316 -0
- data/lib/couch_foo/base.rb +2117 -0
- data/lib/couch_foo/calculations.rb +117 -0
- data/lib/couch_foo/callbacks.rb +311 -0
- data/lib/couch_foo/database.rb +157 -0
- data/lib/couch_foo/dirty.rb +142 -0
- data/lib/couch_foo/named_scope.rb +168 -0
- data/lib/couch_foo/observer.rb +195 -0
- data/lib/couch_foo/reflection.rb +239 -0
- data/lib/couch_foo/timestamp.rb +41 -0
- data/lib/couch_foo/validations.rb +927 -0
- data/lib/couch_foo/view_methods.rb +234 -0
- data/lib/couch_foo.rb +43 -0
- data/test/couch_foo_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +116 -0
@@ -0,0 +1,1118 @@
|
|
1
|
+
require 'couch_foo/associations/association_proxy'
|
2
|
+
require 'couch_foo/associations/association_collection'
|
3
|
+
require 'couch_foo/associations/belongs_to_association'
|
4
|
+
require 'couch_foo/associations/belongs_to_polymorphic_association'
|
5
|
+
require 'couch_foo/associations/has_one_association'
|
6
|
+
require 'couch_foo/associations/has_many_association'
|
7
|
+
#require 'couch_foo/associations/has_many_through_association'
|
8
|
+
require 'couch_foo/associations/has_and_belongs_to_many_association'
|
9
|
+
#require 'couch_foo/associations/has_one_through_association'
|
10
|
+
|
11
|
+
module CouchFoo
|
12
|
+
class HasManyThroughAssociationNotFoundError < CouchFooError #:nodoc:
|
13
|
+
def initialize(owner_class_name, reflection)
|
14
|
+
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class HasManyThroughAssociationPolymorphicError < CouchFooError #:nodoc:
|
19
|
+
def initialize(owner_class_name, reflection, source_reflection)
|
20
|
+
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class HasManyThroughAssociationPointlessSourceTypeError < CouchFooError #:nodoc:
|
25
|
+
def initialize(owner_class_name, reflection, source_reflection)
|
26
|
+
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class HasManyThroughSourceAssociationNotFoundError < CouchFooError #:nodoc:
|
31
|
+
def initialize(reflection)
|
32
|
+
through_reflection = reflection.through_reflection
|
33
|
+
source_reflection_names = reflection.source_reflection_names
|
34
|
+
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
|
35
|
+
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class HasManyThroughSourceAssociationMacroError < CouchFooError #:nodoc:
|
40
|
+
def initialize(reflection)
|
41
|
+
through_reflection = reflection.through_reflection
|
42
|
+
source_reflection = reflection.source_reflection
|
43
|
+
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class HasManyThroughCantAssociateThroughHasManyReflection < CouchFooError #:nodoc:
|
48
|
+
def initialize(owner, reflection)
|
49
|
+
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
class HasManyThroughCantAssociateNewRecords < CouchFooError #:nodoc:
|
53
|
+
def initialize(owner, reflection)
|
54
|
+
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class HasManyThroughCantDissociateNewRecords < CouchFooError #:nodoc:
|
59
|
+
def initialize(owner, reflection)
|
60
|
+
super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class EagerLoadPolymorphicError < CouchFooError #:nodoc:
|
65
|
+
def initialize(reflection)
|
66
|
+
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ReadOnlyAssociation < CouchFooError #:nodoc:
|
71
|
+
def initialize(reflection)
|
72
|
+
super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module Associations # :nodoc:
|
77
|
+
def self.included(base)
|
78
|
+
base.extend(ClassMethods)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clears out the association cache
|
82
|
+
def clear_association_cache #:nodoc:
|
83
|
+
self.class.reflect_on_all_associations.to_a.each do |assoc|
|
84
|
+
instance_variable_set "@#{assoc.name}", nil
|
85
|
+
end unless self.new_record?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
|
89
|
+
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
|
90
|
+
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
|
91
|
+
# methods. Example:
|
92
|
+
#
|
93
|
+
# class Project < CouchFoo::Base
|
94
|
+
# belongs_to :portfolio
|
95
|
+
# has_one :project_manager
|
96
|
+
# has_many :milestones
|
97
|
+
# has_and_belongs_to_many :categories
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
|
101
|
+
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
|
102
|
+
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
|
103
|
+
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
|
104
|
+
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options),</tt>
|
105
|
+
# <tt>Project#milestones.build, Project#milestones.create</tt>
|
106
|
+
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
|
107
|
+
# <tt>Project#categories.delete(category1)</tt>
|
108
|
+
#
|
109
|
+
# === Note
|
110
|
+
#
|
111
|
+
# The current CouchFoo implementation does not include has_and_belongs_to_many This will be added
|
112
|
+
# in a future release along with an option for using has_many in an inline context, so all the
|
113
|
+
# associated documents are stored in the parent itself rather than in separate records.
|
114
|
+
#
|
115
|
+
# === A word of warning
|
116
|
+
#
|
117
|
+
# Don't create associations that have the same name as instance methods of CouchFoo::Base. Since the association
|
118
|
+
# adds a method with that name to its model, it will override the inherited method and break things.
|
119
|
+
# For instance, +attributes+ and +connection+ would be bad choices for association names.
|
120
|
+
#
|
121
|
+
# == Auto-generated methods
|
122
|
+
#
|
123
|
+
# === Singular associations (one-to-one)
|
124
|
+
# | | belongs_to |
|
125
|
+
# generated methods | belongs_to | :polymorphic | has_one
|
126
|
+
# ----------------------------------+------------+--------------+---------
|
127
|
+
# #other | X | X | X
|
128
|
+
# #other=(other) | X | X | X
|
129
|
+
# #build_other(attributes={}) | X | | X
|
130
|
+
# #create_other(attributes={}) | X | | X
|
131
|
+
# #other.create!(attributes={}) | | | X
|
132
|
+
# #other.nil? | X | X |
|
133
|
+
#
|
134
|
+
# ===Collection associations (one-to-many / many-to-many)
|
135
|
+
# | | | has_many
|
136
|
+
# generated methods | habtm | has_many | :through
|
137
|
+
# ----------------------------------+-------+----------+----------
|
138
|
+
# #others | X | X | X
|
139
|
+
# #others=(other,other,...) | X | X | X
|
140
|
+
# #other_ids | X | X | X
|
141
|
+
# #other_ids=(id,id,...) | X | X | X
|
142
|
+
# #others<< | X | X | X
|
143
|
+
# #others.push | X | X | X
|
144
|
+
# #others.concat | X | X | X
|
145
|
+
# #others.build(attributes={}) | X | X | X
|
146
|
+
# #others.create(attributes={}) | X | X | X
|
147
|
+
# #others.create!(attributes={}) | X | X | X
|
148
|
+
# #others.size | X | X | X
|
149
|
+
# #others.length | X | X | X
|
150
|
+
# #others.count | X | X | X
|
151
|
+
# #others.sum(args*,&block) | X | X | X
|
152
|
+
# #others.empty? | X | X | X
|
153
|
+
# #others.clear | X | X | X
|
154
|
+
# #others.delete(other,other,...) | X | X | X
|
155
|
+
# #others.delete_all | X | X |
|
156
|
+
# #others.destroy_all | X | X | X
|
157
|
+
# #others.find(*args) | X | X | X
|
158
|
+
# #others.find_first | X | |
|
159
|
+
# #others.uniq | X | X | X
|
160
|
+
# #others.reset | X | X | X
|
161
|
+
#
|
162
|
+
# == Cardinality and associations
|
163
|
+
#
|
164
|
+
# Couch Foo associations can be used to describe one-to-one, one-to-many and many-to-many
|
165
|
+
# relationships between models. Each model uses an association to describe its role in
|
166
|
+
# the relation. The +belongs_to+ association is always used in the model that has
|
167
|
+
# the foreign key.
|
168
|
+
#
|
169
|
+
# === One-to-one
|
170
|
+
#
|
171
|
+
# Use +has_one+ in the base, and +belongs_to+ in the associated model.
|
172
|
+
#
|
173
|
+
# class Employee < CouchFoo::Base
|
174
|
+
# has_one :office
|
175
|
+
# end
|
176
|
+
# class Office < CouchFoo::Base
|
177
|
+
# belongs_to :employee # foreign key - employee_id
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# === One-to-many
|
181
|
+
#
|
182
|
+
# Use +has_many+ in the base, and +belongs_to+ in the associated model.
|
183
|
+
#
|
184
|
+
# class Manager < CouchFoo::Base
|
185
|
+
# has_many :employees
|
186
|
+
# end
|
187
|
+
# class Employee < CouchFoo::Base
|
188
|
+
# belongs_to :manager # foreign key - manager_id
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# === Many-to-many
|
192
|
+
#
|
193
|
+
# Not implement yet
|
194
|
+
#
|
195
|
+
# == Is it a +belongs_to+ or +has_one+ association?
|
196
|
+
#
|
197
|
+
# Both express a 1-1 relationship. The difference is mostly where to place the foreign key, which goes on the model for the class
|
198
|
+
# declaring the +belongs_to+ relationship. Example:
|
199
|
+
#
|
200
|
+
# class User < CouchFoo::Base
|
201
|
+
# # I reference an account.
|
202
|
+
# belongs_to :account
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# class Account < CouchFoo::Base
|
206
|
+
# # One user references me.
|
207
|
+
# has_one :user
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# The properties definitions for these classes could look something like:
|
211
|
+
# class User < CouchFoo::Base
|
212
|
+
# property :account_id, Integer
|
213
|
+
# property :name, String
|
214
|
+
# end
|
215
|
+
#
|
216
|
+
# class Account < CouchFoo::Base
|
217
|
+
# property :name, String
|
218
|
+
# end
|
219
|
+
#
|
220
|
+
# == Unsaved objects and associations
|
221
|
+
#
|
222
|
+
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
|
223
|
+
# aware of, mostly involving the saving of associated objects.
|
224
|
+
#
|
225
|
+
# === One-to-one associations
|
226
|
+
#
|
227
|
+
# * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in
|
228
|
+
# order to update their primary keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
|
229
|
+
# * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns +false+ and the assignment
|
230
|
+
# is cancelled.
|
231
|
+
# * If you wish to assign an object to a +has_one+ association without saving it, use the <tt>association.build</tt> method (documented below).
|
232
|
+
# * Assigning an object to a +belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It
|
233
|
+
# does not save the parent either.
|
234
|
+
#
|
235
|
+
# === Collections
|
236
|
+
#
|
237
|
+
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically saves that object, except if the parent object
|
238
|
+
# (the owner of the collection) is not yet stored in the database.
|
239
|
+
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) fails, then <tt>push</tt> returns +false+.
|
240
|
+
# * You can add an object to a collection without automatically saving it by using the <tt>collection.build</tt> method (documented below).
|
241
|
+
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically saved when the parent is saved.
|
242
|
+
#
|
243
|
+
# === Association callbacks
|
244
|
+
#
|
245
|
+
# Similar to the normal callbacks that hook into the lifecycle of an Couch Foo object, you can also define callbacks that get
|
246
|
+
# triggered when you add an object to or remove an object from an association collection. Example:
|
247
|
+
#
|
248
|
+
# class Project
|
249
|
+
# has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
|
250
|
+
#
|
251
|
+
# def evaluate_velocity(developer)
|
252
|
+
# ...
|
253
|
+
# end
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# It's possible to stack callbacks by passing them as an array. Example:
|
257
|
+
#
|
258
|
+
# class Project
|
259
|
+
# has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
|
263
|
+
#
|
264
|
+
# Should any of the +before_add+ callbacks throw an exception, the object does not get added to the collection. Same with
|
265
|
+
# the +before_remove+ callbacks; if an exception is thrown the object doesn't get removed.
|
266
|
+
#
|
267
|
+
# === Association extensions
|
268
|
+
#
|
269
|
+
# The proxy objects that control the access to associations can be extended through anonymous modules. This is especially
|
270
|
+
# beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association.
|
271
|
+
# Example:
|
272
|
+
#
|
273
|
+
# class Account < CouchFoo::Base
|
274
|
+
# has_many :people do
|
275
|
+
# def find_or_create_by_name(name)
|
276
|
+
# first_name, last_name = name.split(" ", 2)
|
277
|
+
# find_or_create_by_first_name_and_last_name(first_name, last_name)
|
278
|
+
# end
|
279
|
+
# end
|
280
|
+
# end
|
281
|
+
#
|
282
|
+
# person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
|
283
|
+
# person.first_name # => "David"
|
284
|
+
# person.last_name # => "Heinemeier Hansson"
|
285
|
+
#
|
286
|
+
# If you need to share the same extensions between many associations, you can use a named extension module. Example:
|
287
|
+
#
|
288
|
+
# module FindOrCreateByNameExtension
|
289
|
+
# def find_or_create_by_name(name)
|
290
|
+
# first_name, last_name = name.split(" ", 2)
|
291
|
+
# find_or_create_by_first_name_and_last_name(first_name, last_name)
|
292
|
+
# end
|
293
|
+
# end
|
294
|
+
#
|
295
|
+
# class Account < CouchFoo::Base
|
296
|
+
# has_many :people, :extend => FindOrCreateByNameExtension
|
297
|
+
# end
|
298
|
+
#
|
299
|
+
# class Company < CouchFoo::Base
|
300
|
+
# has_many :people, :extend => FindOrCreateByNameExtension
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
# If you need to use multiple named extension modules, you can specify an array of modules with the <tt>:extend</tt> option.
|
304
|
+
# In the case of name conflicts between methods in the modules, methods in modules later in the array supercede
|
305
|
+
# those earlier in the array. Example:
|
306
|
+
#
|
307
|
+
# class Account < CouchFoo::Base
|
308
|
+
# has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
|
309
|
+
# end
|
310
|
+
#
|
311
|
+
# Some extensions can only be made to work with knowledge of the association proxy's internals.
|
312
|
+
# Extensions can access relevant state using accessors on the association proxy:
|
313
|
+
#
|
314
|
+
# * +proxy_owner+ - Returns the object the association is part of.
|
315
|
+
# * +proxy_reflection+ - Returns the reflection object that describes the association.
|
316
|
+
# * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
|
317
|
+
#
|
318
|
+
# === Association Join Models
|
319
|
+
#
|
320
|
+
# This is not supported yet
|
321
|
+
#
|
322
|
+
# === Polymorphic Associations
|
323
|
+
#
|
324
|
+
# Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
|
325
|
+
# specify an interface that a +has_many+ association must adhere to.
|
326
|
+
#
|
327
|
+
# class Asset < CouchFoo::Base
|
328
|
+
# belongs_to :attachable, :polymorphic => true
|
329
|
+
# end
|
330
|
+
#
|
331
|
+
# class Post < CouchFoo::Base
|
332
|
+
# has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
|
333
|
+
# end
|
334
|
+
#
|
335
|
+
# @asset.attachable = @post
|
336
|
+
#
|
337
|
+
# This works by using a type property in addition to a foreign key to specify the associated record. In the Asset example, you'd need
|
338
|
+
# an +attachable_id+ key attribute and an +attachable_type+ string attribute.
|
339
|
+
#
|
340
|
+
# Using polymorphic associations in combination with inheritance is a little tricky. In order
|
341
|
+
# for the associations to work as expected, ensure that you store the base model in the
|
342
|
+
# type property of the polymorphic association. To continue with the asset example above, suppose
|
343
|
+
# there are guest posts and member posts that use inheritence. In this case, there must be a +type+
|
344
|
+
# property in the Post model.
|
345
|
+
#
|
346
|
+
# class Asset < CouchFoo::Base
|
347
|
+
# belongs_to :attachable, :polymorphic => true
|
348
|
+
#
|
349
|
+
# def attachable_type=(sType)
|
350
|
+
# super(sType.to_s.classify.constantize.class.to_s)
|
351
|
+
# end
|
352
|
+
# end
|
353
|
+
#
|
354
|
+
# class Post < CouchFoo::Base
|
355
|
+
# # because we store "Post" in attachable_type now :dependent => :destroy will work
|
356
|
+
# has_many :assets, :as => :attachable, :dependent => :destroy
|
357
|
+
# end
|
358
|
+
#
|
359
|
+
# class GuestPost < Post
|
360
|
+
# end
|
361
|
+
#
|
362
|
+
# class MemberPost < Post
|
363
|
+
# end
|
364
|
+
#
|
365
|
+
# == Caching
|
366
|
+
#
|
367
|
+
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
368
|
+
# instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
|
369
|
+
# worrying too much about performance at the first go. Example:
|
370
|
+
#
|
371
|
+
# project.milestones # fetches milestones from the database
|
372
|
+
# project.milestones.size # uses the milestone cache
|
373
|
+
# project.milestones.empty? # uses the milestone cache
|
374
|
+
# project.milestones(true).size # fetches milestones from the database
|
375
|
+
# project.milestones # uses the milestone cache
|
376
|
+
#
|
377
|
+
# == Eager loading of associations
|
378
|
+
#
|
379
|
+
# Not implemented yet
|
380
|
+
#
|
381
|
+
# == Modules
|
382
|
+
#
|
383
|
+
# By default, associations will look for objects within the current module scope. Consider:
|
384
|
+
#
|
385
|
+
# module MyApplication
|
386
|
+
# module Business
|
387
|
+
# class Firm < CouchFoo::Base
|
388
|
+
# has_many :clients
|
389
|
+
# end
|
390
|
+
#
|
391
|
+
# class Company < CouchFoo::Base; end
|
392
|
+
# end
|
393
|
+
# end
|
394
|
+
#
|
395
|
+
# When Firm#clients is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
|
396
|
+
# with a class in another module scope, this can be done by specifying the complete class name. Example:
|
397
|
+
#
|
398
|
+
# module MyApplication
|
399
|
+
# module Business
|
400
|
+
# class Firm < CouchFoo::Base; end
|
401
|
+
# end
|
402
|
+
#
|
403
|
+
# module Billing
|
404
|
+
# class Account < CouchFoo::Base
|
405
|
+
# belongs_to :firm, :class_name => "MyApplication::Business::Firm"
|
406
|
+
# end
|
407
|
+
# end
|
408
|
+
# end
|
409
|
+
#
|
410
|
+
# == Type safety with <tt>CouchFoo::AssociationTypeMismatch</tt>
|
411
|
+
#
|
412
|
+
# If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
|
413
|
+
# get an <tt>CouchFoo::AssociationTypeMismatch</tt>.
|
414
|
+
#
|
415
|
+
# == Options
|
416
|
+
#
|
417
|
+
# All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones
|
418
|
+
# possible.
|
419
|
+
module ClassMethods
|
420
|
+
# Adds the following methods for retrieval and query of collections of associated objects:
|
421
|
+
# +collection+ is replaced with the symbol passed as the first argument, so
|
422
|
+
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
423
|
+
# * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
|
424
|
+
# An empty array is returned if none are found.
|
425
|
+
# * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
426
|
+
# * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
427
|
+
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
|
428
|
+
# * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
|
429
|
+
# * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
|
430
|
+
# * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
|
431
|
+
# * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
|
432
|
+
# are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
|
433
|
+
# otherwise sets their foreign keys to +NULL+.
|
434
|
+
# * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
|
435
|
+
# * <tt>collection.size</tt> - Returns the number of associated objects.
|
436
|
+
# * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
|
437
|
+
# * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
|
438
|
+
# with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
|
439
|
+
# associated object already exists, not if it's +nil+!
|
440
|
+
# * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
441
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
442
|
+
# *Note:* This only works if an associated object already exists, not if it's +nil+!
|
443
|
+
#
|
444
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
445
|
+
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
|
446
|
+
# * <tt>Firm#clients<<</tt>
|
447
|
+
# * <tt>Firm#clients.delete</tt>
|
448
|
+
# * <tt>Firm#clients=</tt>
|
449
|
+
# * <tt>Firm#client_ids</tt>
|
450
|
+
# * <tt>Firm#client_ids=</tt>
|
451
|
+
# * <tt>Firm#clients.clear</tt>
|
452
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
453
|
+
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
454
|
+
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
455
|
+
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
456
|
+
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
457
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
458
|
+
#
|
459
|
+
# Options are:
|
460
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
461
|
+
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
|
462
|
+
# if the real class name is SpecialProduct, you'll have to specify it with this option.
|
463
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included
|
464
|
+
# in the results. For example <tt>has_many :posts, :conditions => {:published => true}</tt>. This will also
|
465
|
+
# create published posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
|
466
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned by a property to sort on,
|
467
|
+
# for example :order => :product_weight. See notes in CouchFoo#find when using with :limit
|
468
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
|
469
|
+
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
470
|
+
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
471
|
+
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
|
472
|
+
# the <tt>:through</tt> option.
|
473
|
+
# * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
|
474
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
|
475
|
+
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. See notes
|
476
|
+
# in CouchFoo#find when using with :order
|
477
|
+
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
478
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
479
|
+
# * <tt>:through</tt> - Not implemented at the moment
|
480
|
+
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
|
481
|
+
# association is a polymorphic +belongs_to+.
|
482
|
+
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
483
|
+
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
484
|
+
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
|
485
|
+
#
|
486
|
+
# Option examples:
|
487
|
+
# has_many :comments, :order => :posted_on
|
488
|
+
# has_many :comments, :include => :author
|
489
|
+
# has_many :people, :class_name => "Person", :conditions => {deleted => 0}, :order => "name"
|
490
|
+
# has_many :tracks, :order => :position, :dependent => :destroy
|
491
|
+
# has_many :comments, :dependent => :nullify
|
492
|
+
# has_many :tags, :as => :taggable
|
493
|
+
# has_many :reports, :readonly => true
|
494
|
+
def has_many(association_id, options = {}, &extension)
|
495
|
+
reflection = create_has_many_reflection(association_id, options, &extension)
|
496
|
+
configure_dependency_for_has_many(reflection)
|
497
|
+
|
498
|
+
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
|
499
|
+
add_multiple_associated_save_callbacks(reflection.name)
|
500
|
+
add_association_callbacks(reflection.name, reflection.options)
|
501
|
+
|
502
|
+
#if options[:through]
|
503
|
+
# collection_accessor_methods(reflection, HasManyThroughAssociation)
|
504
|
+
#else
|
505
|
+
collection_accessor_methods(reflection, HasManyAssociation)
|
506
|
+
#end
|
507
|
+
end
|
508
|
+
|
509
|
+
# Adds the following methods for retrieval and query of a single associated object:
|
510
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
511
|
+
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
|
512
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
513
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, sets it as the foreign key,
|
514
|
+
# and saves the associate object.
|
515
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
516
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
517
|
+
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if
|
518
|
+
# an association already exists. It will NOT work if the association is +nil+.
|
519
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
520
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
521
|
+
#
|
522
|
+
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
523
|
+
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
|
524
|
+
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
525
|
+
# * <tt>Account#beneficiary.nil?</tt>
|
526
|
+
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
527
|
+
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
528
|
+
#
|
529
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
530
|
+
#
|
531
|
+
# Options are:
|
532
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
533
|
+
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
|
534
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
535
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included
|
536
|
+
# in the results. For example <tt>has_many :posts, :conditions => {:published => true}</tt>. This will also
|
537
|
+
# create published posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
|
538
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned by a property to sort on,
|
539
|
+
# for example :order => :product_weight. See notes in CouchFoo#find when using with :limit
|
540
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
541
|
+
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
|
542
|
+
# object's foreign key is set to +NULL+. Also, association is assigned.
|
543
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
544
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id"
|
545
|
+
# as the default <tt>:foreign_key</tt>.
|
546
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
547
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
548
|
+
# * <tt>:through</tt> - Not implemented yet
|
549
|
+
# * <tt>:source</tt> - Not implemented yet
|
550
|
+
# * <tt>:source_type</tt> - Not implemented yet
|
551
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
552
|
+
# * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
|
553
|
+
#
|
554
|
+
# Option examples:
|
555
|
+
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
|
556
|
+
# has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it
|
557
|
+
# has_one :last_comment, :class_name => "Comment", :order => :posted_on
|
558
|
+
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
559
|
+
# has_one :attachment, :as => :attachable
|
560
|
+
# has_one :boss, :readonly => :true
|
561
|
+
def has_one(association_id, options = {})
|
562
|
+
#if options[:through]
|
563
|
+
# reflection = create_has_one_through_reflection(association_id, options)
|
564
|
+
# association_accessor_methods(reflection, CouchFoo::Associations::HasOneThroughAssociation)
|
565
|
+
#else
|
566
|
+
reflection = create_has_one_reflection(association_id, options)
|
567
|
+
|
568
|
+
ivar = "@#{reflection.name}"
|
569
|
+
|
570
|
+
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
|
571
|
+
define_method(method_name) do
|
572
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
573
|
+
|
574
|
+
if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
|
575
|
+
association["#{reflection.primary_key_name}"] = id
|
576
|
+
association.save(true)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
after_save method_name
|
580
|
+
|
581
|
+
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
582
|
+
association_accessor_methods(reflection, HasOneAssociation)
|
583
|
+
association_constructor_method(:build, reflection, HasOneAssociation)
|
584
|
+
association_constructor_method(:create, reflection, HasOneAssociation)
|
585
|
+
|
586
|
+
configure_dependency_for_has_one(reflection)
|
587
|
+
#end
|
588
|
+
end
|
589
|
+
|
590
|
+
# Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
|
591
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
592
|
+
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
|
593
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
594
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
595
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
596
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
597
|
+
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
|
598
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
599
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
600
|
+
#
|
601
|
+
# Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
|
602
|
+
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
603
|
+
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
604
|
+
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
605
|
+
# * <tt>Post#author.nil?</tt>
|
606
|
+
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
607
|
+
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
608
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
609
|
+
#
|
610
|
+
# Options are:
|
611
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
612
|
+
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
|
613
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
614
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included
|
615
|
+
# in the results. For example <tt>has_many :posts, :conditions => {:published => true}</tt>. This will also
|
616
|
+
# create published posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
|
617
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
618
|
+
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
|
619
|
+
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
|
620
|
+
# will use a foreign key of "favorite_person_id".
|
621
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
622
|
+
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
|
623
|
+
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
|
624
|
+
# orphaned records behind.
|
625
|
+
# * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
|
626
|
+
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
|
627
|
+
# destroyed. This requires that a property named <tt>#{document_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
628
|
+
# is used on the associate class (such as a Post class). You can also specify a custom counter cache property by providing
|
629
|
+
# a property name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
630
|
+
# When creating a counter cache property, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
|
631
|
+
# this results in a counter with +NULL+ value, which will never increment.
|
632
|
+
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
|
633
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
634
|
+
# * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
|
635
|
+
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
636
|
+
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
637
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
638
|
+
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
|
639
|
+
#
|
640
|
+
# Option examples:
|
641
|
+
# belongs_to :firm, :foreign_key => "client_of"
|
642
|
+
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
643
|
+
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
644
|
+
# :conditions => {discounts = #{payments_count}}
|
645
|
+
# belongs_to :attachable, :polymorphic => true
|
646
|
+
# belongs_to :project, :readonly => true
|
647
|
+
# belongs_to :post, :counter_cache => true
|
648
|
+
def belongs_to(association_id, options = {})
|
649
|
+
reflection = create_belongs_to_reflection(association_id, options)
|
650
|
+
|
651
|
+
ivar = "@#{reflection.name}"
|
652
|
+
|
653
|
+
if reflection.options[:polymorphic]
|
654
|
+
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
655
|
+
|
656
|
+
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
|
657
|
+
define_method(method_name) do
|
658
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
659
|
+
|
660
|
+
if association && association.target
|
661
|
+
if association.new_record?
|
662
|
+
association.save(true)
|
663
|
+
end
|
664
|
+
|
665
|
+
if association.updated?
|
666
|
+
self["#{reflection.primary_key_name}"] = association.id
|
667
|
+
self["#{reflection.options[:foreign_type]}"] = association.class.name.to_s
|
668
|
+
end
|
669
|
+
end
|
670
|
+
end
|
671
|
+
before_save method_name
|
672
|
+
else
|
673
|
+
association_accessor_methods(reflection, BelongsToAssociation)
|
674
|
+
association_constructor_method(:build, reflection, BelongsToAssociation)
|
675
|
+
association_constructor_method(:create, reflection, BelongsToAssociation)
|
676
|
+
|
677
|
+
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
|
678
|
+
define_method(method_name) do
|
679
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
680
|
+
|
681
|
+
if !association.nil?
|
682
|
+
if association.new_record?
|
683
|
+
association.save(true)
|
684
|
+
end
|
685
|
+
|
686
|
+
if association.updated?
|
687
|
+
self["#{reflection.primary_key_name}"] = association.id
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
before_save method_name
|
692
|
+
end
|
693
|
+
|
694
|
+
# Create the callbacks to update counter cache
|
695
|
+
if options[:counter_cache]
|
696
|
+
cache_property = options[:counter_cache] == true ?
|
697
|
+
"#{self.to_s.demodulize.underscore.pluralize}_count" :
|
698
|
+
options[:counter_cache]
|
699
|
+
|
700
|
+
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
701
|
+
define_method(method_name) do
|
702
|
+
association = send("#{reflection.name}")
|
703
|
+
association.class.increment_counter("#{cache_property}", send("#{reflection.primary_key_name}")) unless association.nil?
|
704
|
+
end
|
705
|
+
after_create method_name
|
706
|
+
|
707
|
+
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
708
|
+
define_method(method_name) do
|
709
|
+
association = send("#{reflection.name}")
|
710
|
+
association.class.decrement_counter("#{cache_property}", send("#{reflection.primary_key_name}")) unless association.nil?
|
711
|
+
end
|
712
|
+
before_destroy method_name
|
713
|
+
|
714
|
+
module_eval(
|
715
|
+
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_property}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
716
|
+
)
|
717
|
+
end
|
718
|
+
|
719
|
+
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
720
|
+
|
721
|
+
configure_dependency_for_belongs_to(reflection)
|
722
|
+
end
|
723
|
+
|
724
|
+
|
725
|
+
# def has_and_belongs_to_many(association_id, options = {}, &extension)
|
726
|
+
# reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
727
|
+
#
|
728
|
+
# add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
|
729
|
+
# add_multiple_associated_save_callbacks(reflection.name)
|
730
|
+
# collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
|
731
|
+
#
|
732
|
+
# # Don't use a before_destroy callback since users' before_destroy
|
733
|
+
# # callbacks will be executed after the association is wiped out.
|
734
|
+
# old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
|
735
|
+
# class_eval <<-end_eval unless method_defined?(old_method)
|
736
|
+
# alias_method :#{old_method}, :destroy_without_callbacks
|
737
|
+
# def destroy_without_callbacks
|
738
|
+
# #{reflection.name}.clear
|
739
|
+
# #{old_method}
|
740
|
+
# end
|
741
|
+
# end_eval
|
742
|
+
#
|
743
|
+
# add_association_callbacks(reflection.name, options)
|
744
|
+
# end
|
745
|
+
|
746
|
+
private
|
747
|
+
def association_accessor_methods(reflection, association_proxy_class)
|
748
|
+
ivar = "@#{reflection.name}"
|
749
|
+
|
750
|
+
define_method(reflection.name) do |*params|
|
751
|
+
force_reload = params.first unless params.empty?
|
752
|
+
|
753
|
+
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
754
|
+
|
755
|
+
if association.nil? || force_reload
|
756
|
+
association = association_proxy_class.new(self, reflection)
|
757
|
+
retval = association.reload
|
758
|
+
if retval.nil? and association_proxy_class == BelongsToAssociation
|
759
|
+
instance_variable_set(ivar, nil)
|
760
|
+
return nil
|
761
|
+
end
|
762
|
+
instance_variable_set(ivar, association)
|
763
|
+
end
|
764
|
+
|
765
|
+
association.target.nil? ? nil : association
|
766
|
+
end
|
767
|
+
|
768
|
+
define_method("#{reflection.name}=") do |new_value|
|
769
|
+
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
770
|
+
|
771
|
+
if association.nil? || association.target != new_value
|
772
|
+
association = association_proxy_class.new(self, reflection)
|
773
|
+
end
|
774
|
+
|
775
|
+
# if association_proxy_class == HasOneThroughAssociation
|
776
|
+
# association.create_through_record(new_value)
|
777
|
+
# self.send(reflection.name, new_value)
|
778
|
+
# else
|
779
|
+
association.replace(new_value)
|
780
|
+
instance_variable_set(ivar, new_value.nil? ? nil : association)
|
781
|
+
# end
|
782
|
+
end
|
783
|
+
|
784
|
+
define_method("set_#{reflection.name}_target") do |target|
|
785
|
+
return if target.nil? and association_proxy_class == BelongsToAssociation
|
786
|
+
association = association_proxy_class.new(self, reflection)
|
787
|
+
association.target = target
|
788
|
+
instance_variable_set(ivar, association)
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
def collection_reader_method(reflection, association_proxy_class)
|
793
|
+
define_method(reflection.name) do |*params|
|
794
|
+
ivar = "@#{reflection.name}"
|
795
|
+
|
796
|
+
force_reload = params.first unless params.empty?
|
797
|
+
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
798
|
+
|
799
|
+
unless association.respond_to?(:loaded?)
|
800
|
+
association = association_proxy_class.new(self, reflection)
|
801
|
+
instance_variable_set(ivar, association)
|
802
|
+
end
|
803
|
+
|
804
|
+
association.reload if force_reload
|
805
|
+
|
806
|
+
association
|
807
|
+
end
|
808
|
+
|
809
|
+
define_method("#{reflection.name.to_s.singularize}_ids") do
|
810
|
+
send(reflection.name).map(&:id)
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
|
815
|
+
collection_reader_method(reflection, association_proxy_class)
|
816
|
+
|
817
|
+
if writer
|
818
|
+
define_method("#{reflection.name}=") do |new_value|
|
819
|
+
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
|
820
|
+
association = send(reflection.name)
|
821
|
+
association.replace(new_value)
|
822
|
+
association
|
823
|
+
end
|
824
|
+
|
825
|
+
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
826
|
+
ids = (new_value || []).reject { |nid| nid.blank? }
|
827
|
+
send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
|
828
|
+
end
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
def add_single_associated_validation_callbacks(association_name)
|
833
|
+
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
834
|
+
define_method(method_name) do
|
835
|
+
association = instance_variable_get("@#{association_name}")
|
836
|
+
if !association.nil?
|
837
|
+
errors.add "#{association_name}" unless association.target.nil? || association.valid?
|
838
|
+
end
|
839
|
+
end
|
840
|
+
|
841
|
+
validate method_name
|
842
|
+
end
|
843
|
+
|
844
|
+
def add_multiple_associated_validation_callbacks(association_name)
|
845
|
+
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
846
|
+
ivar = "@#{association_name}"
|
847
|
+
|
848
|
+
define_method(method_name) do
|
849
|
+
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
850
|
+
|
851
|
+
if association.respond_to?(:loaded?)
|
852
|
+
if new_record?
|
853
|
+
association
|
854
|
+
elsif association.loaded?
|
855
|
+
association.select { |record| record.new_record? }
|
856
|
+
else
|
857
|
+
association.target.select { |record| record.new_record? }
|
858
|
+
end.each do |record|
|
859
|
+
errors.add "#{association_name}" unless record.valid?
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
validate method_name
|
865
|
+
end
|
866
|
+
|
867
|
+
def add_multiple_associated_save_callbacks(association_name)
|
868
|
+
ivar = "@#{association_name}"
|
869
|
+
|
870
|
+
method_name = "before_save_associated_records_for_#{association_name}".to_sym
|
871
|
+
define_method(method_name) do
|
872
|
+
@new_record_before_save = new_record?
|
873
|
+
true
|
874
|
+
end
|
875
|
+
before_save method_name
|
876
|
+
|
877
|
+
method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
|
878
|
+
define_method(method_name) do
|
879
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
880
|
+
|
881
|
+
records_to_save = if @new_record_before_save
|
882
|
+
association
|
883
|
+
elsif association.respond_to?(:loaded?) && association.loaded?
|
884
|
+
association.select { |record| record.new_record? }
|
885
|
+
elsif association.respond_to?(:loaded?) && !association.loaded?
|
886
|
+
association.target.select { |record| record.new_record? }
|
887
|
+
else
|
888
|
+
[]
|
889
|
+
end
|
890
|
+
records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
|
891
|
+
|
892
|
+
# reconstruct the conditions now that we know the owner's id
|
893
|
+
association.send(:construct_conditions) if association.respond_to?(:construct_conditions)
|
894
|
+
end
|
895
|
+
|
896
|
+
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
897
|
+
after_create method_name
|
898
|
+
after_update method_name
|
899
|
+
end
|
900
|
+
|
901
|
+
def association_constructor_method(constructor, reflection, association_proxy_class)
|
902
|
+
define_method("#{constructor}_#{reflection.name}") do |*params|
|
903
|
+
ivar = "@#{reflection.name}"
|
904
|
+
|
905
|
+
attributees = params.first unless params.empty?
|
906
|
+
replace_existing = params[1].nil? ? true : params[1]
|
907
|
+
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
908
|
+
|
909
|
+
if association.nil?
|
910
|
+
association = association_proxy_class.new(self, reflection)
|
911
|
+
instance_variable_set(ivar, association)
|
912
|
+
end
|
913
|
+
|
914
|
+
if association_proxy_class == HasOneAssociation
|
915
|
+
association.send(constructor, attributees, replace_existing)
|
916
|
+
else
|
917
|
+
association.send(constructor, attributees)
|
918
|
+
end
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
# See HasManyAssociation#delete_records. Dependent associations
|
923
|
+
# delete children, otherwise foreign key is set to NULL.
|
924
|
+
def configure_dependency_for_has_many(reflection)
|
925
|
+
if reflection.options.include?(:dependent)
|
926
|
+
case reflection.options[:dependent]
|
927
|
+
when :destroy
|
928
|
+
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
929
|
+
define_method(method_name) do
|
930
|
+
send("#{reflection.name}").each { |o| o.destroy }
|
931
|
+
end
|
932
|
+
before_destroy method_name
|
933
|
+
when :delete_all
|
934
|
+
method_name = "has_many_dependent_delete_for_#{reflection.name}".to_sym
|
935
|
+
define_method(method_name) do
|
936
|
+
send("#{reflection.name}").each { |o| o.delete }
|
937
|
+
end
|
938
|
+
before_destroy method_name
|
939
|
+
when :nullify
|
940
|
+
method_name = "has_many_dependent_nullify_for_#{reflection.name}".to_sym
|
941
|
+
define_method(method_name) do
|
942
|
+
send("#{reflection.name}").each { |o| o.update_attribute({reflection.primary_key_name.to_sym => nil}, true) }
|
943
|
+
self.class.database.commit
|
944
|
+
end
|
945
|
+
before_destroy method_name
|
946
|
+
else
|
947
|
+
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
|
948
|
+
end
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
def configure_dependency_for_has_one(reflection)
|
953
|
+
if reflection.options.include?(:dependent)
|
954
|
+
case reflection.options[:dependent]
|
955
|
+
when :destroy
|
956
|
+
method_name = "has_one_dependent_destroy_for_#{reflection.name}".to_sym
|
957
|
+
define_method(method_name) do
|
958
|
+
association = send("#{reflection.name}")
|
959
|
+
association.destroy unless association.nil?
|
960
|
+
end
|
961
|
+
before_destroy method_name
|
962
|
+
when :delete
|
963
|
+
method_name = "has_one_dependent_delete_for_#{reflection.name}".to_sym
|
964
|
+
define_method(method_name) do
|
965
|
+
association = send("#{reflection.name}")
|
966
|
+
association.class.delete(association.id) unless association.nil?
|
967
|
+
end
|
968
|
+
before_destroy method_name
|
969
|
+
when :nullify
|
970
|
+
method_name = "has_one_dependent_nullify_for_#{reflection.name}".to_sym
|
971
|
+
define_method(method_name) do
|
972
|
+
association = send("#{reflection.name}")
|
973
|
+
association.update_attribute("#{reflection.primary_key_name}", nil) unless association.nil?
|
974
|
+
end
|
975
|
+
before_destroy method_name
|
976
|
+
else
|
977
|
+
raise ArgumentError, "The :dependent option expects either :destroy, :delete or :nullify (#{reflection.options[:dependent].inspect})"
|
978
|
+
end
|
979
|
+
end
|
980
|
+
end
|
981
|
+
|
982
|
+
def configure_dependency_for_belongs_to(reflection)
|
983
|
+
if reflection.options.include?(:dependent)
|
984
|
+
case reflection.options[:dependent]
|
985
|
+
when :destroy
|
986
|
+
method_name = "belongs_to_dependent_destroy_for_#{reflection.name}".to_sym
|
987
|
+
define_method(method_name) do
|
988
|
+
association = send("#{reflection.name}")
|
989
|
+
association.destroy unless association.nil?
|
990
|
+
end
|
991
|
+
before_destroy method_name
|
992
|
+
when :delete
|
993
|
+
method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym
|
994
|
+
define_method(method_name) do
|
995
|
+
association = send("#{reflection.name}")
|
996
|
+
association.class.delete(association.id) unless association.nil?
|
997
|
+
end
|
998
|
+
before_destroy method_name
|
999
|
+
else
|
1000
|
+
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def create_has_many_reflection(association_id, options, &extension)
|
1006
|
+
options.assert_valid_keys(
|
1007
|
+
:class_name, :foreign_key, :dependent,
|
1008
|
+
:conditions, :include, :order, :limit, :count, :offset, :skip,
|
1009
|
+
:as, :through, :source, :source_type,
|
1010
|
+
:uniq,
|
1011
|
+
:before_add, :after_add, :before_remove, :after_remove,
|
1012
|
+
:extend, :readonly,
|
1013
|
+
:validate,
|
1014
|
+
:startkey, :endkey, :keys, :view_type, :descending, :startkey_docid, :endkey_docid
|
1015
|
+
)
|
1016
|
+
|
1017
|
+
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
1018
|
+
|
1019
|
+
create_reflection(:has_many, association_id, options, self)
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
def create_has_one_reflection(association_id, options)
|
1023
|
+
options.assert_valid_keys(
|
1024
|
+
:class_name, :foreign_key, :remote, :conditions, :order, :include,
|
1025
|
+
:dependent, :counter_cache, :extend, :as, :readonly, :validate
|
1026
|
+
)
|
1027
|
+
|
1028
|
+
create_reflection(:has_one, association_id, options, self)
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
#def create_has_one_through_reflection(association_id, options)
|
1032
|
+
# options.assert_valid_keys(
|
1033
|
+
# :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
|
1034
|
+
# )
|
1035
|
+
# create_reflection(:has_one, association_id, options, self)
|
1036
|
+
#end
|
1037
|
+
|
1038
|
+
def create_belongs_to_reflection(association_id, options)
|
1039
|
+
options.assert_valid_keys(
|
1040
|
+
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
|
1041
|
+
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, :validate
|
1042
|
+
)
|
1043
|
+
|
1044
|
+
reflection = create_reflection(:belongs_to, association_id, options, self)
|
1045
|
+
|
1046
|
+
if options[:polymorphic]
|
1047
|
+
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
reflection
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
1054
|
+
options.assert_valid_keys(
|
1055
|
+
:class_name, :foreign_key, :association_foreign_key,
|
1056
|
+
:conditions, :include, :order, :group, :offset, :skip,
|
1057
|
+
:uniq,
|
1058
|
+
:before_add, :after_add, :before_remove, :after_remove,
|
1059
|
+
:extend, :readonly,
|
1060
|
+
:validate,
|
1061
|
+
:startkey, :endkey, :keys, :view_type, :descending, :startkey_docid, :endkey_docid
|
1062
|
+
)
|
1063
|
+
|
1064
|
+
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
1065
|
+
|
1066
|
+
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
|
1067
|
+
# TODO rename join_table when get here
|
1068
|
+
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
|
1069
|
+
|
1070
|
+
reflection
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
def reflect_on_included_associations(associations)
|
1074
|
+
[ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def guard_against_unlimitable_reflections(reflections, options)
|
1078
|
+
if (options[:offset] || options[:limit] || options[:count]) && !using_limitable_reflections?(reflections)
|
1079
|
+
raise(
|
1080
|
+
ConfigurationError,
|
1081
|
+
"You can not use offset and limit together with has_many or has_and_belongs_to_many associations"
|
1082
|
+
)
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
def using_limitable_reflections?(reflections)
|
1087
|
+
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
def add_association_callbacks(association_name, options)
|
1091
|
+
callbacks = %w(before_add after_add before_remove after_remove)
|
1092
|
+
callbacks.each do |callback_name|
|
1093
|
+
full_callback_name = "#{callback_name}_for_#{association_name}"
|
1094
|
+
defined_callbacks = options[callback_name.to_sym]
|
1095
|
+
if options.has_key?(callback_name.to_sym)
|
1096
|
+
class_inheritable_reader full_callback_name.to_sym
|
1097
|
+
write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten)
|
1098
|
+
else
|
1099
|
+
write_inheritable_attribute(full_callback_name.to_sym, [])
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def create_extension_modules(association_id, block_extension, extensions)
|
1105
|
+
if block_extension
|
1106
|
+
extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension"
|
1107
|
+
|
1108
|
+
silence_warnings do
|
1109
|
+
self.parent.const_set(extension_module_name, Module.new(&block_extension))
|
1110
|
+
end
|
1111
|
+
Array(extensions).push("#{self.parent}::#{extension_module_name}".constantize)
|
1112
|
+
else
|
1113
|
+
Array(extensions)
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
end
|