duck_record 0.0.20 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
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