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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d6d9bbb4be3b78789d5908e035bac681c78f4c7c
4
- data.tar.gz: bc20716f86bdda0d3454fabf2b625107f273c3dc
3
+ metadata.gz: 2f7887df2a7d513fd1089691ef8f669cfa26c335
4
+ data.tar.gz: 18a3664afcdac4bd096b2d93e69ca42fcf7b52f5
5
5
  SHA512:
6
- metadata.gz: a87eab5c7972d7dbddef1bd72d9faa72792e6f06c7dd49066b4b8a7ff9761e9cf393a11086827ce74fc9ac931c6a36d611bcf1efd26f78253451a3cf15f7e1d9
7
- data.tar.gz: d76ca8431973b036caed6f1e0f8a1129e509f0c0a6338c0a7238959bc16024a5d971bc150c30af0c424ad985326b0229781bbfdba80e7199373a69e74d570f05
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 :EmbedsManyAssociation
28
- autoload :EmbedsOneAssociation
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 :EmbedsAssociation, "duck_record/associations/builder/embeds_association"
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, "duck_record/associations/builder/embeds_one"
34
- autoload :EmbedsMany, "duck_record/associations/builder/embeds_many"
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
- def self.eager_load!
38
- super
39
- Preloader.eager_load!
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