duck_record 0.0.20 → 0.0.21
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.
- checksums.yaml +4 -4
- data/lib/duck_record/associations.rb +41 -347
- data/lib/duck_record/associations/association.rb +267 -0
- data/lib/duck_record/associations/belongs_to_association.rb +71 -0
- data/lib/duck_record/associations/builder/{embeds_association.rb → association.rb} +37 -9
- data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
- data/lib/duck_record/associations/builder/collection_association.rb +45 -0
- data/lib/duck_record/associations/builder/embeds_many.rb +1 -44
- data/lib/duck_record/associations/builder/embeds_one.rb +1 -26
- data/lib/duck_record/associations/builder/has_many.rb +11 -0
- data/lib/duck_record/associations/builder/has_one.rb +20 -0
- data/lib/duck_record/associations/builder/singular_association.rb +33 -0
- data/lib/duck_record/associations/collection_association.rb +476 -0
- data/lib/duck_record/associations/collection_proxy.rb +1160 -0
- data/lib/duck_record/associations/foreign_association.rb +11 -0
- data/lib/duck_record/associations/has_many_association.rb +17 -0
- data/lib/duck_record/associations/has_one_association.rb +39 -0
- data/lib/duck_record/associations/singular_association.rb +73 -0
- data/lib/duck_record/nested_validate_association.rb +1 -1
- data/lib/duck_record/reflection.rb +345 -8
- data/lib/duck_record/version.rb +1 -1
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f7887df2a7d513fd1089691ef8f669cfa26c335
|
4
|
+
data.tar.gz: 18a3664afcdac4bd096b2d93e69ca42fcf7b52f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9bb37f630dd759ed2b392f3193355623ef4990da1a575be15011cb0fe3b3d0ecc7ba52e1167cabf57f319786f00ea8de473a76cbf218cb099e2c795b2df2c70
|
7
|
+
data.tar.gz: 3f09e0a8051a1181967d5e688580132cabec39280210e3031cded9044a49142c28e536c318f6c630a9d4060434bd04b656cd3f2ad4cc2cab5a7760eb79d8a484
|
@@ -24,19 +24,35 @@ module DuckRecord
|
|
24
24
|
autoload :EmbedsAssociation
|
25
25
|
autoload :EmbedsManyProxy
|
26
26
|
|
27
|
-
autoload :
|
28
|
-
autoload :
|
27
|
+
autoload :Association
|
28
|
+
autoload :SingularAssociation
|
29
|
+
autoload :CollectionAssociation
|
30
|
+
autoload :ForeignAssociation
|
31
|
+
autoload :CollectionProxy
|
32
|
+
autoload :ThroughAssociation
|
29
33
|
|
30
34
|
module Builder #:nodoc:
|
31
|
-
autoload :
|
35
|
+
autoload :Association, "duck_record/associations/builder/association"
|
36
|
+
autoload :SingularAssociation, "duck_record/associations/builder/singular_association"
|
37
|
+
autoload :CollectionAssociation, "duck_record/associations/builder/collection_association"
|
32
38
|
|
33
|
-
autoload :EmbedsOne,
|
34
|
-
autoload :EmbedsMany,
|
39
|
+
autoload :EmbedsOne, "duck_record/associations/builder/embeds_one"
|
40
|
+
autoload :EmbedsMany, "duck_record/associations/builder/embeds_many"
|
41
|
+
|
42
|
+
autoload :BelongsTo, "duck_record/associations/builder/belongs_to"
|
43
|
+
autoload :HasOne, "duck_record/associations/builder/has_one"
|
44
|
+
autoload :HasMany, "duck_record/associations/builder/has_many"
|
35
45
|
end
|
36
46
|
|
37
|
-
|
38
|
-
|
39
|
-
|
47
|
+
eager_autoload do
|
48
|
+
autoload :EmbedsManyAssociation
|
49
|
+
autoload :EmbedsOneAssociation
|
50
|
+
|
51
|
+
autoload :BelongsToAssociation
|
52
|
+
autoload :HasOneAssociation
|
53
|
+
autoload :HasOneThroughAssociation
|
54
|
+
autoload :HasManyAssociation
|
55
|
+
autoload :HasManyThroughAssociation
|
40
56
|
end
|
41
57
|
|
42
58
|
# Returns the association instance for the given name, instantiating it if it doesn't already exist
|
@@ -85,350 +101,28 @@ module DuckRecord
|
|
85
101
|
end
|
86
102
|
|
87
103
|
module ClassMethods
|
88
|
-
# Specifies a one-to-many association. The following methods for retrieval and query of
|
89
|
-
# collections of associated objects will be added:
|
90
|
-
#
|
91
|
-
# +collection+ is a placeholder for the symbol passed as the +name+ argument, so
|
92
|
-
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
93
|
-
#
|
94
|
-
# [collection(force_reload = false)]
|
95
|
-
# Returns an array of all the associated objects.
|
96
|
-
# An empty array is returned if none are found.
|
97
|
-
# [collection<<(object, ...)]
|
98
|
-
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
99
|
-
# Note that this operation instantly fires update SQL without waiting for the save or update call on the
|
100
|
-
# parent object, unless the parent object is a new record.
|
101
|
-
# This will also run validations and callbacks of associated object(s).
|
102
|
-
# [collection.delete(object, ...)]
|
103
|
-
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
104
|
-
# Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
|
105
|
-
# and deleted if they're associated with <tt>dependent: :delete_all</tt>.
|
106
|
-
#
|
107
|
-
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
|
108
|
-
# nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
|
109
|
-
# <tt>dependent: :nullify</tt> to override this.
|
110
|
-
# [collection.destroy(object, ...)]
|
111
|
-
# Removes one or more objects from the collection by running <tt>destroy</tt> on
|
112
|
-
# each record, regardless of any dependent option, ensuring callbacks are run.
|
113
|
-
#
|
114
|
-
# If the <tt>:through</tt> option is used, then the join records are destroyed
|
115
|
-
# instead, not the objects themselves.
|
116
|
-
# [collection=objects]
|
117
|
-
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
|
118
|
-
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
|
119
|
-
# direct by default. You can specify <tt>dependent: :destroy</tt> or
|
120
|
-
# <tt>dependent: :nullify</tt> to override this.
|
121
|
-
# [collection_singular_ids]
|
122
|
-
# Returns an array of the associated objects' ids
|
123
|
-
# [collection_singular_ids=ids]
|
124
|
-
# Replace the collection with the objects identified by the primary keys in +ids+. This
|
125
|
-
# method loads the models and calls <tt>collection=</tt>. See above.
|
126
|
-
# [collection.clear]
|
127
|
-
# Removes every object from the collection. This destroys the associated objects if they
|
128
|
-
# are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
|
129
|
-
# database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
|
130
|
-
# If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
|
131
|
-
# Join models are directly deleted.
|
132
|
-
# [collection.empty?]
|
133
|
-
# Returns +true+ if there are no associated objects.
|
134
|
-
# [collection.size]
|
135
|
-
# Returns the number of associated objects.
|
136
|
-
# [collection.find(...)]
|
137
|
-
# Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find.
|
138
|
-
# [collection.exists?(...)]
|
139
|
-
# Checks whether an associated object with the given conditions exists.
|
140
|
-
# Uses the same rules as ActiveRecord::FinderMethods#exists?.
|
141
|
-
# [collection.build(attributes = {}, ...)]
|
142
|
-
# Returns one or more new objects of the collection type that have been instantiated
|
143
|
-
# with +attributes+ and linked to this object through a foreign key, but have not yet
|
144
|
-
# been saved.
|
145
|
-
# [collection.create(attributes = {})]
|
146
|
-
# Returns a new object of the collection type that has been instantiated
|
147
|
-
# with +attributes+, linked to this object through a foreign key, and that has already
|
148
|
-
# been saved (if it passed the validation). *Note*: This only works if the base model
|
149
|
-
# already exists in the DB, not if it is a new (unsaved) record!
|
150
|
-
# [collection.create!(attributes = {})]
|
151
|
-
# Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid
|
152
|
-
# if the record is invalid.
|
153
|
-
#
|
154
|
-
# === Example
|
155
|
-
#
|
156
|
-
# A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
|
157
|
-
# * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
|
158
|
-
# * <tt>Firm#clients<<</tt>
|
159
|
-
# * <tt>Firm#clients.delete</tt>
|
160
|
-
# * <tt>Firm#clients.destroy</tt>
|
161
|
-
# * <tt>Firm#clients=</tt>
|
162
|
-
# * <tt>Firm#client_ids</tt>
|
163
|
-
# * <tt>Firm#client_ids=</tt>
|
164
|
-
# * <tt>Firm#clients.clear</tt>
|
165
|
-
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
166
|
-
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
167
|
-
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
|
168
|
-
# * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
|
169
|
-
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
170
|
-
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
171
|
-
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
|
172
|
-
# The declaration can also include an +options+ hash to specialize the behavior of the association.
|
173
|
-
#
|
174
|
-
# === Scopes
|
175
|
-
#
|
176
|
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
|
177
|
-
# lambda) to retrieve a specific set of records or customize the generated
|
178
|
-
# query when you access the associated collection.
|
179
|
-
#
|
180
|
-
# Scope examples:
|
181
|
-
# has_many :comments, -> { where(author_id: 1) }
|
182
|
-
# has_many :employees, -> { joins(:address) }
|
183
|
-
# has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
|
184
|
-
#
|
185
|
-
# === Extensions
|
186
|
-
#
|
187
|
-
# The +extension+ argument allows you to pass a block into a has_many
|
188
|
-
# association. This is useful for adding new finders, creators and other
|
189
|
-
# factory-type methods to be used as part of the association.
|
190
|
-
#
|
191
|
-
# Extension examples:
|
192
|
-
# has_many :employees do
|
193
|
-
# def find_or_create_by_name(name)
|
194
|
-
# first_name, last_name = name.split(" ", 2)
|
195
|
-
# find_or_create_by(first_name: first_name, last_name: last_name)
|
196
|
-
# end
|
197
|
-
# end
|
198
|
-
#
|
199
|
-
# === Options
|
200
|
-
# [:class_name]
|
201
|
-
# Specify the class name of the association. Use it only if that name can't be inferred
|
202
|
-
# from the association name. So <tt>has_many :products</tt> will by default be linked
|
203
|
-
# to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to
|
204
|
-
# specify it with this option.
|
205
|
-
# [:foreign_key]
|
206
|
-
# Specify the foreign key used for the association. By default this is guessed to be the name
|
207
|
-
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many
|
208
|
-
# association will use "person_id" as the default <tt>:foreign_key</tt>.
|
209
|
-
# [:foreign_type]
|
210
|
-
# Specify the column used to store the associated object's type, if this is a polymorphic
|
211
|
-
# association. By default this is guessed to be the name of the polymorphic association
|
212
|
-
# specified on "as" option with a "_type" suffix. So a class that defines a
|
213
|
-
# <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
|
214
|
-
# default <tt>:foreign_type</tt>.
|
215
|
-
# [:primary_key]
|
216
|
-
# Specify the name of the column to use as the primary key for the association. By default this is +id+.
|
217
|
-
# [:dependent]
|
218
|
-
# Controls what happens to the associated objects when
|
219
|
-
# their owner is destroyed. Note that these are implemented as
|
220
|
-
# callbacks, and Rails executes callbacks in order. Therefore, other
|
221
|
-
# similar callbacks may affect the <tt>:dependent</tt> behavior, and the
|
222
|
-
# <tt>:dependent</tt> behavior may affect other callbacks.
|
223
|
-
#
|
224
|
-
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
|
225
|
-
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
|
226
|
-
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
|
227
|
-
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
|
228
|
-
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
|
229
|
-
#
|
230
|
-
# If using with the <tt>:through</tt> option, the association on the join model must be
|
231
|
-
# a #belongs_to, and the records which get deleted are the join records, rather than
|
232
|
-
# the associated records.
|
233
|
-
#
|
234
|
-
# If using <tt>dependent: :destroy</tt> on a scoped association, only the scoped objects are destroyed.
|
235
|
-
# For example, if a Post model defines
|
236
|
-
# <tt>has_many :comments, -> { where published: true }, dependent: :destroy</tt> and <tt>destroy</tt> is
|
237
|
-
# called on a post, only published comments are destroyed. This means that any unpublished comments in the
|
238
|
-
# database would still contain a foreign key pointing to the now deleted post.
|
239
|
-
# [:counter_cache]
|
240
|
-
# This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
|
241
|
-
# when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association.
|
242
|
-
# [:as]
|
243
|
-
# Specifies a polymorphic interface (See #belongs_to).
|
244
|
-
# [:through]
|
245
|
-
# Specifies an association through which to perform the query. This can be any other type
|
246
|
-
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
|
247
|
-
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
|
248
|
-
# source reflection.
|
249
|
-
#
|
250
|
-
# If the association on the join model is a #belongs_to, the collection can be modified
|
251
|
-
# and the records on the <tt>:through</tt> model will be automatically created and removed
|
252
|
-
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
|
253
|
-
# <tt>:through</tt> association directly.
|
254
|
-
#
|
255
|
-
# If you are going to modify the association (rather than just read from it), then it is
|
256
|
-
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
|
257
|
-
# join model. This allows associated records to be built which will automatically create
|
258
|
-
# the appropriate join model records when they are saved. (See the 'Association Join Models'
|
259
|
-
# section above.)
|
260
|
-
# [:source]
|
261
|
-
# Specifies the source association name used by #has_many <tt>:through</tt> queries.
|
262
|
-
# Only use it if the name cannot be inferred from the association.
|
263
|
-
# <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
|
264
|
-
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
|
265
|
-
# [:source_type]
|
266
|
-
# Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source
|
267
|
-
# association is a polymorphic #belongs_to.
|
268
|
-
# [:validate]
|
269
|
-
# When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
|
270
|
-
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
|
271
|
-
# [:autosave]
|
272
|
-
# If true, always save the associated objects or destroy them if marked for destruction,
|
273
|
-
# when saving the parent object. If false, never save or destroy the associated objects.
|
274
|
-
# By default, only save associated objects that are new records. This option is implemented as a
|
275
|
-
# +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
|
276
|
-
# may need to be explicitly saved in any user-defined +before_save+ callbacks.
|
277
|
-
#
|
278
|
-
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
|
279
|
-
# <tt>:autosave</tt> to <tt>true</tt>.
|
280
|
-
# [:inverse_of]
|
281
|
-
# Specifies the name of the #belongs_to association on the associated object
|
282
|
-
# that is the inverse of this #has_many association. Does not work in combination
|
283
|
-
# with <tt>:through</tt> or <tt>:as</tt> options.
|
284
|
-
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
|
285
|
-
# [:extend]
|
286
|
-
# Specifies a module or array of modules that will be extended into the association object returned.
|
287
|
-
# Useful for defining methods on associations, especially when they should be shared between multiple
|
288
|
-
# association objects.
|
289
|
-
#
|
290
|
-
# Option examples:
|
291
|
-
# has_many :comments, -> { order("posted_on") }
|
292
|
-
# has_many :comments, -> { includes(:author) }
|
293
|
-
# has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
|
294
|
-
# has_many :tracks, -> { order("position") }, dependent: :destroy
|
295
|
-
# has_many :comments, dependent: :nullify
|
296
|
-
# has_many :tags, as: :taggable
|
297
|
-
# has_many :reports, -> { readonly }
|
298
|
-
# has_many :subscribers, through: :subscriptions, source: :user
|
299
104
|
def embeds_many(name, options = {}, &extension)
|
300
|
-
reflection = Builder::EmbedsMany.build(self, name, options, &extension)
|
105
|
+
reflection = Builder::EmbedsMany.build(self, name, nil, options, &extension)
|
301
106
|
Reflection.add_reflection self, name, reflection
|
302
107
|
end
|
303
108
|
|
304
|
-
# Specifies a one-to-one association with another class. This method should only be used
|
305
|
-
# if the other class contains the foreign key. If the current class contains the foreign key,
|
306
|
-
# then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview
|
307
|
-
# on when to use #has_one and when to use #belongs_to.
|
308
|
-
#
|
309
|
-
# The following methods for retrieval and query of a single associated object will be added:
|
310
|
-
#
|
311
|
-
# +association+ is a placeholder for the symbol passed as the +name+ argument, so
|
312
|
-
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
|
313
|
-
#
|
314
|
-
# [association(force_reload = false)]
|
315
|
-
# Returns the associated object. +nil+ is returned if none is found.
|
316
|
-
# [association=(associate)]
|
317
|
-
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
|
318
|
-
# and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
|
319
|
-
# associated object when assigning a new one, even if the new one isn't saved to database.
|
320
|
-
# [build_association(attributes = {})]
|
321
|
-
# Returns a new object of the associated type that has been instantiated
|
322
|
-
# with +attributes+ and linked to this object through a foreign key, but has not
|
323
|
-
# yet been saved.
|
324
|
-
# [create_association(attributes = {})]
|
325
|
-
# Returns a new object of the associated type that has been instantiated
|
326
|
-
# with +attributes+, linked to this object through a foreign key, and that
|
327
|
-
# has already been saved (if it passed the validation).
|
328
|
-
# [create_association!(attributes = {})]
|
329
|
-
# Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
|
330
|
-
# if the record is invalid.
|
331
|
-
#
|
332
|
-
# === Example
|
333
|
-
#
|
334
|
-
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
335
|
-
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
|
336
|
-
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
337
|
-
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
338
|
-
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
339
|
-
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
|
340
|
-
#
|
341
|
-
# === Scopes
|
342
|
-
#
|
343
|
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
|
344
|
-
# lambda) to retrieve a specific record or customize the generated query
|
345
|
-
# when you access the associated object.
|
346
|
-
#
|
347
|
-
# Scope examples:
|
348
|
-
# has_one :author, -> { where(comment_id: 1) }
|
349
|
-
# has_one :employer, -> { joins(:company) }
|
350
|
-
# has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
|
351
|
-
#
|
352
|
-
# === Options
|
353
|
-
#
|
354
|
-
# The declaration can also include an +options+ hash to specialize the behavior of the association.
|
355
|
-
#
|
356
|
-
# Options are:
|
357
|
-
# [:class_name]
|
358
|
-
# Specify the class name of the association. Use it only if that name can't be inferred
|
359
|
-
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
|
360
|
-
# if the real class name is Person, you'll have to specify it with this option.
|
361
|
-
# [:dependent]
|
362
|
-
# Controls what happens to the associated object when
|
363
|
-
# its owner is destroyed:
|
364
|
-
#
|
365
|
-
# * <tt>:destroy</tt> causes the associated object to also be destroyed
|
366
|
-
# * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
|
367
|
-
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
|
368
|
-
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
|
369
|
-
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
|
370
|
-
#
|
371
|
-
# Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
|
372
|
-
# [:foreign_key]
|
373
|
-
# Specify the foreign key used for the association. By default this is guessed to be the name
|
374
|
-
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association
|
375
|
-
# will use "person_id" as the default <tt>:foreign_key</tt>.
|
376
|
-
# [:foreign_type]
|
377
|
-
# Specify the column used to store the associated object's type, if this is a polymorphic
|
378
|
-
# association. By default this is guessed to be the name of the polymorphic association
|
379
|
-
# specified on "as" option with a "_type" suffix. So a class that defines a
|
380
|
-
# <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
|
381
|
-
# default <tt>:foreign_type</tt>.
|
382
|
-
# [:primary_key]
|
383
|
-
# Specify the method that returns the primary key used for the association. By default this is +id+.
|
384
|
-
# [:as]
|
385
|
-
# Specifies a polymorphic interface (See #belongs_to).
|
386
|
-
# [:through]
|
387
|
-
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
|
388
|
-
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
|
389
|
-
# source reflection. You can only use a <tt>:through</tt> query through a #has_one
|
390
|
-
# or #belongs_to association on the join model.
|
391
|
-
# [:source]
|
392
|
-
# Specifies the source association name used by #has_one <tt>:through</tt> queries.
|
393
|
-
# Only use it if the name cannot be inferred from the association.
|
394
|
-
# <tt>has_one :favorite, through: :favorites</tt> will look for a
|
395
|
-
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
|
396
|
-
# [:source_type]
|
397
|
-
# Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source
|
398
|
-
# association is a polymorphic #belongs_to.
|
399
|
-
# [:validate]
|
400
|
-
# When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
|
401
|
-
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
|
402
|
-
# [:autosave]
|
403
|
-
# If true, always save the associated object or destroy it if marked for destruction,
|
404
|
-
# when saving the parent object. If false, never save or destroy the associated object.
|
405
|
-
# By default, only save the associated object if it's a new record.
|
406
|
-
#
|
407
|
-
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
|
408
|
-
# <tt>:autosave</tt> to <tt>true</tt>.
|
409
|
-
# [:inverse_of]
|
410
|
-
# Specifies the name of the #belongs_to association on the associated object
|
411
|
-
# that is the inverse of this #has_one association. Does not work in combination
|
412
|
-
# with <tt>:through</tt> or <tt>:as</tt> options.
|
413
|
-
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
|
414
|
-
# [:required]
|
415
|
-
# When set to +true+, the association will also have its presence validated.
|
416
|
-
# This will validate the association itself, not the id. You can use
|
417
|
-
# +:inverse_of+ to avoid an extra query during validation.
|
418
|
-
#
|
419
|
-
# Option examples:
|
420
|
-
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
|
421
|
-
# has_one :credit_card, dependent: :nullify # updates the associated records foreign
|
422
|
-
# # key value to NULL rather than destroying it
|
423
|
-
# has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
|
424
|
-
# has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
|
425
|
-
# has_one :attachment, as: :attachable
|
426
|
-
# has_one :boss, -> { readonly }
|
427
|
-
# has_one :club, through: :membership
|
428
|
-
# has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
|
429
|
-
# has_one :credit_card, required: true
|
430
109
|
def embeds_one(name, options = {})
|
431
|
-
reflection = Builder::EmbedsOne.build(self, name, options)
|
110
|
+
reflection = Builder::EmbedsOne.build(self, name, nil, options)
|
111
|
+
Reflection.add_reflection self, name, reflection
|
112
|
+
end
|
113
|
+
|
114
|
+
def belongs_to(name, scope = nil, options = {})
|
115
|
+
reflection = Builder::BelongsTo.build(self, name, scope, options)
|
116
|
+
Reflection.add_reflection self, name, reflection
|
117
|
+
end
|
118
|
+
|
119
|
+
def has_one(name, scope = nil, options = {})
|
120
|
+
reflection = Builder::HasOne.build(self, name, scope, options)
|
121
|
+
Reflection.add_reflection self, name, reflection
|
122
|
+
end
|
123
|
+
|
124
|
+
def has_many(name, scope = nil, options = {}, &extension)
|
125
|
+
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
|
432
126
|
Reflection.add_reflection self, name, reflection
|
433
127
|
end
|
434
128
|
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require "active_support/core_ext/array/wrap"
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
module Associations
|
5
|
+
# = Active Record Associations
|
6
|
+
#
|
7
|
+
# This is the root class of all associations ('+ Foo' signifies an included module Foo):
|
8
|
+
#
|
9
|
+
# Association
|
10
|
+
# SingularAssociation
|
11
|
+
# HasOneAssociation + ForeignAssociation
|
12
|
+
# HasOneThroughAssociation + ThroughAssociation
|
13
|
+
# BelongsToAssociation
|
14
|
+
# BelongsToPolymorphicAssociation
|
15
|
+
# CollectionAssociation
|
16
|
+
# HasManyAssociation + ForeignAssociation
|
17
|
+
# HasManyThroughAssociation + ThroughAssociation
|
18
|
+
class Association #:nodoc:
|
19
|
+
attr_reader :owner, :target, :reflection
|
20
|
+
|
21
|
+
delegate :options, to: :reflection
|
22
|
+
|
23
|
+
def initialize(owner, reflection)
|
24
|
+
reflection.check_validity!
|
25
|
+
|
26
|
+
@owner, @reflection = owner, reflection
|
27
|
+
|
28
|
+
reset
|
29
|
+
reset_scope
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the name of the table of the associated class:
|
33
|
+
#
|
34
|
+
# post.comments.aliased_table_name # => "comments"
|
35
|
+
#
|
36
|
+
def aliased_table_name
|
37
|
+
klass.table_name
|
38
|
+
end
|
39
|
+
|
40
|
+
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
41
|
+
def reset
|
42
|
+
@loaded = false
|
43
|
+
@target = nil
|
44
|
+
@stale_state = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Reloads the \target and returns +self+ on success.
|
48
|
+
def reload
|
49
|
+
reset
|
50
|
+
reset_scope
|
51
|
+
load_target
|
52
|
+
self unless target.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Has the \target been already \loaded?
|
56
|
+
def loaded?
|
57
|
+
@loaded
|
58
|
+
end
|
59
|
+
|
60
|
+
# Asserts the \target has been loaded setting the \loaded flag to +true+.
|
61
|
+
def loaded!
|
62
|
+
@loaded = true
|
63
|
+
@stale_state = stale_state
|
64
|
+
end
|
65
|
+
|
66
|
+
# The target is stale if the target no longer points to the record(s) that the
|
67
|
+
# relevant foreign_key(s) refers to. If stale, the association accessor method
|
68
|
+
# on the owner will reload the target. It's up to subclasses to implement the
|
69
|
+
# stale_state method if relevant.
|
70
|
+
#
|
71
|
+
# Note that if the target has not been loaded, it is not considered stale.
|
72
|
+
def stale_target?
|
73
|
+
loaded? && @stale_state != stale_state
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
|
77
|
+
def target=(target)
|
78
|
+
@target = target
|
79
|
+
loaded!
|
80
|
+
end
|
81
|
+
|
82
|
+
def scope
|
83
|
+
target_scope.merge!(association_scope)
|
84
|
+
end
|
85
|
+
|
86
|
+
# The scope for this association.
|
87
|
+
#
|
88
|
+
# Note that the association_scope is merged into the target_scope only when the
|
89
|
+
# scope method is called. This is because at that point the call may be surrounded
|
90
|
+
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
|
91
|
+
# actually gets built.
|
92
|
+
def association_scope
|
93
|
+
if klass
|
94
|
+
@association_scope ||= ActiveRecord::Associations::AssociationScope.scope(self, klass.connection)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def reset_scope
|
99
|
+
@association_scope = nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set the inverse association, if possible
|
103
|
+
def set_inverse_instance(record)
|
104
|
+
record
|
105
|
+
end
|
106
|
+
|
107
|
+
# Remove the inverse association, if possible
|
108
|
+
def remove_inverse_instance(_record); end
|
109
|
+
|
110
|
+
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
|
111
|
+
# polymorphic_type field on the owner.
|
112
|
+
def klass
|
113
|
+
reflection.klass
|
114
|
+
end
|
115
|
+
|
116
|
+
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
|
117
|
+
# through association's scope)
|
118
|
+
def target_scope
|
119
|
+
ActiveRecord::AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
|
120
|
+
end
|
121
|
+
|
122
|
+
def extensions
|
123
|
+
extensions = klass.default_extensions | reflection.extensions
|
124
|
+
|
125
|
+
if scope = reflection.scope
|
126
|
+
extensions |= klass.unscoped.instance_exec(owner, &scope).extensions
|
127
|
+
end
|
128
|
+
|
129
|
+
extensions
|
130
|
+
end
|
131
|
+
|
132
|
+
# Loads the \target if needed and returns it.
|
133
|
+
#
|
134
|
+
# This method is abstract in the sense that it relies on +find_target+,
|
135
|
+
# which is expected to be provided by descendants.
|
136
|
+
#
|
137
|
+
# If the \target is already \loaded it is just returned. Thus, you can call
|
138
|
+
# +load_target+ unconditionally to get the \target.
|
139
|
+
#
|
140
|
+
# ActiveRecord::RecordNotFound is rescued within the method, and it is
|
141
|
+
# not reraised. The proxy is \reset and +nil+ is the return value.
|
142
|
+
def load_target
|
143
|
+
@target = find_target if (@stale_state && stale_target?) || find_target?
|
144
|
+
|
145
|
+
loaded! unless loaded?
|
146
|
+
target
|
147
|
+
rescue ActiveRecord::RecordNotFound
|
148
|
+
reset
|
149
|
+
end
|
150
|
+
|
151
|
+
def interpolate(sql, record = nil)
|
152
|
+
if sql.respond_to?(:to_proc)
|
153
|
+
owner.instance_exec(record, &sql)
|
154
|
+
else
|
155
|
+
sql
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# We can't dump @reflection since it contains the scope proc
|
160
|
+
def marshal_dump
|
161
|
+
ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
|
162
|
+
[@reflection.name, ivars]
|
163
|
+
end
|
164
|
+
|
165
|
+
def marshal_load(data)
|
166
|
+
reflection_name, ivars = data
|
167
|
+
ivars.each { |name, val| instance_variable_set(name, val) }
|
168
|
+
@reflection = @owner.class._reflect_on_association(reflection_name)
|
169
|
+
end
|
170
|
+
|
171
|
+
def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
|
172
|
+
except_from_scope_attributes ||= {}
|
173
|
+
skip_assign = [reflection.foreign_key, reflection.type].compact
|
174
|
+
assigned_keys = record.changed_attribute_names_to_save
|
175
|
+
assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
|
176
|
+
attributes = create_scope.except(*(assigned_keys - skip_assign))
|
177
|
+
record.assign_attributes(attributes)
|
178
|
+
end
|
179
|
+
|
180
|
+
def create(attributes = {}, &block)
|
181
|
+
_create_record(attributes, &block)
|
182
|
+
end
|
183
|
+
|
184
|
+
def create!(attributes = {}, &block)
|
185
|
+
_create_record(attributes, true, &block)
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def find_target?
|
191
|
+
!loaded? && foreign_key_present? && klass
|
192
|
+
end
|
193
|
+
|
194
|
+
def creation_attributes
|
195
|
+
attributes = {}
|
196
|
+
|
197
|
+
if (reflection.has_one? || reflection.collection?) && !options[:through]
|
198
|
+
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
199
|
+
|
200
|
+
if reflection.options[:as]
|
201
|
+
attributes[reflection.type] = owner.class.base_class.name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
attributes
|
206
|
+
end
|
207
|
+
|
208
|
+
# Sets the owner attributes on the given record
|
209
|
+
def set_owner_attributes(record)
|
210
|
+
creation_attributes.each { |key, value| record[key] = value }
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns true if there is a foreign key present on the owner which
|
214
|
+
# references the target. This is used to determine whether we can load
|
215
|
+
# the target if the owner is currently a new record (and therefore
|
216
|
+
# without a key). If the owner is a new record then foreign_key must
|
217
|
+
# be present in order to load target.
|
218
|
+
#
|
219
|
+
# Currently implemented by belongs_to (vanilla and polymorphic) and
|
220
|
+
# has_one/has_many :through associations which go through a belongs_to.
|
221
|
+
def foreign_key_present?
|
222
|
+
false
|
223
|
+
end
|
224
|
+
|
225
|
+
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
|
226
|
+
# the kind of the class of the associated objects. Meant to be used as
|
227
|
+
# a sanity check when you are about to assign an associated record.
|
228
|
+
def raise_on_type_mismatch!(record)
|
229
|
+
unless record.is_a?(reflection.klass)
|
230
|
+
fresh_class = reflection.class_name.safe_constantize
|
231
|
+
unless fresh_class && record.is_a?(fresh_class)
|
232
|
+
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
|
233
|
+
"got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
|
234
|
+
raise DuckRecord::AssociationTypeMismatch, message
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns true if record contains the foreign_key
|
240
|
+
def foreign_key_for?(record)
|
241
|
+
record.has_attribute?(reflection.foreign_key)
|
242
|
+
end
|
243
|
+
|
244
|
+
# This should be implemented to return the values of the relevant key(s) on the owner,
|
245
|
+
# so that when stale_state is different from the value stored on the last find_target,
|
246
|
+
# the target is stale.
|
247
|
+
#
|
248
|
+
# This is only relevant to certain associations, which is why it returns +nil+ by default.
|
249
|
+
def stale_state
|
250
|
+
end
|
251
|
+
|
252
|
+
def build_record(attributes)
|
253
|
+
reflection.build_association(attributes) do |record|
|
254
|
+
initialize_attributes(record, attributes)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns true if statement cache should be skipped on the association reader.
|
259
|
+
def skip_statement_cache?
|
260
|
+
reflection.has_scope? ||
|
261
|
+
scope.eager_loading? ||
|
262
|
+
klass.scope_attributes? ||
|
263
|
+
reflection.source_reflection.active_record.try(:default_scopes)&.any?
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|