Skip to content

Commit 884fade

Browse files
authored
MONGOID-4843 don't populate .changed_attributes setting has_and_belongs_to_many association (#5276)
* MONGOID-4843 .changed_attributes populated on setting has_and_belongs_to_many association * MONGOID-4843 add note to the documentation * MONGOID-4843 fix spacing * MONGOID-4843 change back to previous solution * Update lib/mongoid/fields.rb * MONGOID-4843 clear foreign key changes * MONGOID-4843 test and document changes to _id(s)
1 parent 0bd32b3 commit 884fade

File tree

3 files changed

+131
-47
lines changed

3 files changed

+131
-47
lines changed

docs/reference/crud.txt

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ operations with examples.
4141

4242
*Insert a document or multiple documents into the database, raising an
4343
error if a validation or server error occurs.*
44-
44+
4545
*Pass a hash of attributes to create one document with the specified
4646
attributes, or an array of hashes to create multiple documents.
4747
If a single hash is passed, the corresponding document is returned.
4848
If an array of hashes is passed, an array of documents corresponding
4949
to the hashes is returned.*
50-
50+
5151
*If a block is given to* ``create!`` *, it will be invoked with each
5252
document as the argument in turn prior to attempting to save that
5353
document.*
54-
54+
5555
*If there is a problem saving any of the documents, such as
5656
a validation error or a server error, an exception is raised
5757
and, consequently, none of the documents are returned.
@@ -78,12 +78,12 @@ operations with examples.
7878

7979
*Instantiate a document or multiple documents and, if validations pass,
8080
insert them into the database.*
81-
81+
8282
``create`` *is similar to* ``create!`` *but does not raise
8383
exceptions on validation errors. It still raises errors on server
8484
errors, such as trying to insert a document with an* ``_id`` *that
8585
already exists in the collection.*
86-
86+
8787
*If any validation errors are encountered, the respective document
8888
is not inserted but is returned along with documents that were inserted.
8989
Use* ``persisted?`` *,* ``new_record?`` *or* ``errors`` *methods
@@ -105,13 +105,13 @@ operations with examples.
105105
Person.create(first_name: "Heinrich") do |doc|
106106
doc.last_name = "Heine"
107107
end # => Person instance
108-
108+
109109
class Post
110110
include Mongoid::Document
111-
111+
112112
validates_uniqueness_of :title
113113
end
114-
114+
115115
posts = Post.create([{title: "test"}, {title: "test"}])
116116
# => array of two Post instances
117117
posts.map { |post| post.persisted? } # => [true, false]
@@ -120,7 +120,7 @@ operations with examples.
120120

121121
*Save the changed attributes to the database atomically, or insert the document if
122122
new. Raises an exception if validations fail or there is a server error.*
123-
123+
124124
*Returns true if the changed attributes were saved, raises an exception otherwise.*
125125
-
126126
.. code-block:: ruby
@@ -138,12 +138,12 @@ operations with examples.
138138

139139
*Save the changed attributes to the database atomically, or insert the document
140140
if new.*
141-
141+
142142
*Returns true if the changed attributes were saved. Returns false
143143
if there were any validation errors. Raises an exception if
144144
the document passed validation but there was a server error during
145145
the save.*
146-
146+
147147
*Pass* ``validate: false`` *option to bypass validations.*
148148
-
149149
.. code-block:: ruby
@@ -210,7 +210,7 @@ operations with examples.
210210
provided time field. This will cascade the touch to all*
211211
``belongs_to`` *associations of the document with the option set.
212212
This operation skips validations and callbacks.*
213-
213+
214214
*Attempting to touch a destroyed document will raise* ``FrozenError``
215215
* (as of Ruby 2.5,* ``RuntimeError`` *on previous Ruby versions),
216216
same as if attempting to update an attribute on a destroyed
@@ -224,14 +224,14 @@ operations with examples.
224224
* - ``Model#delete``
225225

226226
*Deletes the document from the database without running callbacks.*
227-
227+
228228
*If the document is not persisted, Mongoid will attempt to delete from
229229
the database any document with the same* ``_id``.
230230
-
231231
.. code-block:: ruby
232232

233233
person.delete
234-
234+
235235
person = Person.create!(...)
236236
unsaved_person = Person.new(id: person.id)
237237
unsaved_person.delete
@@ -241,14 +241,14 @@ operations with examples.
241241
* - ``Model#destroy``
242242

243243
*Deletes the document from the database while running destroy callbacks.*
244-
244+
245245
*If the document is not persisted, Mongoid will attempt to delete from
246246
the database any document with the same* ``_id``.
247247
-
248248
.. code-block:: ruby
249249

250250
person.destroy
251-
251+
252252
person = Person.create!(...)
253253
unsaved_person = Person.new(id: person.id)
254254
unsaved_person.destroy
@@ -399,9 +399,9 @@ are not invoked.
399399
*Updates an attribute on the model instance and, if the instance
400400
is already persisted, performs an atomic $set on the field, bypassing
401401
validations.*
402-
402+
403403
``set`` *can also deeply set values on Hash fields.*
404-
404+
405405
``set`` *can also deeply set values on* ``embeds_one`` *associations.
406406
If such an association's document is nil, one will be created prior
407407
to the update.*
@@ -424,14 +424,14 @@ are not invoked.
424424

425425
class Post
426426
include Mongoid::Document
427-
427+
428428
field :metadata, type: Hash
429429
end
430-
430+
431431
post = Post.create!
432432
post.set('metadata.published_at' => Time.now)
433433
post.metadata['published_at'] # => Time instance
434-
434+
435435
post.set('metadata.approved.today' => true)
436436
post.metadata['approved'] # => {'today' => true}
437437

@@ -449,7 +449,7 @@ are not invoked.
449449

450450
field :route, type: String
451451
end
452-
452+
453453
flight = Flight.create!
454454
flight.plan # => nil
455455
flight.set('plan.route', 'test route')
@@ -578,18 +578,18 @@ associations would be loaded from the database at the next access.
578578
does not make any changes to the document:
579579

580580
.. code-block:: ruby
581-
581+
582582
# Assuming band has many tours, which could be referenced:
583583
band = Band.create!(tours: [Tour.create!])
584584
# ... or embedded:
585585
band = Band.create!(tours: [Tour.new])
586-
586+
587587
# This writes the empty tour list into the database.
588588
band.tours = []
589-
589+
590590
# There are no unsaved modifications in band at this point to be reverted.
591591
band.reload
592-
592+
593593
# Returns the empty array since this is what is in the database.
594594
band.tours
595595
# => []
@@ -612,7 +612,7 @@ example demonstrates:
612612

613613
Mongoid.raise_not_found_error = false
614614
band.destroy
615-
615+
616616
band.reload
617617
# => #<Band _id: 6206d031e1b8324561f179c8, name: nil, description: nil, likes: nil>
618618

@@ -630,7 +630,7 @@ specified in the document (and the shard key value, if a shard key is defined):
630630
.. code-block:: ruby
631631

632632
existing = Band.create!(name: 'Photek')
633-
633+
634634
# Unsaved document
635635
band = Band.new(id: existing.id)
636636
band.reload
@@ -665,9 +665,9 @@ each declared field:
665665

666666
field :first_name
667667
end
668-
668+
669669
person = Person.new
670-
670+
671671
person.first_name = "Artem"
672672
person.first_name
673673
# => "Artem"
@@ -693,18 +693,18 @@ write the values directly into the attributes hash:
693693
def first_name
694694
read_attribute(:fn)
695695
end
696-
696+
697697
def first_name=(value)
698698
write_attribute(:fn, value)
699699
end
700700
end
701-
701+
702702
person = Person.new
703-
703+
704704
person.first_name = "Artem"
705705
person.first_name
706706
# => "Artem"
707-
707+
708708
person.attributes
709709
# => {"_id"=>BSON::ObjectId('606477dc2c97a628cf47075b'), "fn"=>"Artem"}
710710

@@ -727,7 +727,7 @@ accept either the declared field name or the storage field name for operations:
727727
field :first_name, as: :fn
728728
field :last_name, as: :ln
729729
end
730-
730+
731731
person = Person.new(first_name: "Artem")
732732
# => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): nil>
733733

@@ -736,11 +736,11 @@ accept either the declared field name or the storage field name for operations:
736736

737737
person.read_attribute(:fn)
738738
# => "Artem"
739-
739+
740740
person.write_attribute(:last_name, "Pushkin")
741741
person
742742
# => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): "Pushkin">
743-
743+
744744
person.write_attribute(:ln, "Medvedev")
745745
person
746746
# => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): "Medvedev">
@@ -756,7 +756,7 @@ does not cause the respective field to be defined either:
756756
# => #<Person _id: 60647b212c97a6292c195b4c, first_name(fn): "Artem", last_name(ln): "Medvedev">
757757
person.attributes
758758
# => {"_id"=>BSON::ObjectId('60647b212c97a6292c195b4c'), "first_name"=>"Artem", "last_name"=>"Medvedev", "undefined"=>"Hello"}
759-
759+
760760
person.read_attribute(:undefined)
761761
# => "Hello"
762762
person.undefined
@@ -784,17 +784,17 @@ for the detailed description of their behavior.
784784
end
785785

786786
person = Person.new(first_name: "Artem")
787-
787+
788788
person["fn"]
789789
# => "Artem"
790-
790+
791791
person[:first_name]
792792
# => "Artem"
793-
793+
794794
person[:ln] = "Medvedev"
795795
person
796796
# => #<Person _id: 606483742c97a629bdde5cfc, first_name(fn): "Artem", last_name(ln): "Medvedev">
797-
797+
798798
person["last_name"] = "Pushkin"
799799
person
800800
# => #<Person _id: 606483742c97a629bdde5cfc, first_name(fn): "Artem", last_name(ln): "Pushkin">
@@ -863,6 +863,14 @@ the database up to the time it is saved. Any persistence operation clears the ch
863863
# Get the previous value for a field.
864864
person.name_was # "Alan Parsons"
865865

866+
.. note::
867+
868+
Setting the associations on a document does not cause the ``changes`` or
869+
``changed_attributes`` hashes to be modified. This is true for all associations
870+
whether referenced or embedded. Note that changing the _id(s) field on
871+
referenced associations does cause the changes to show up in the ``changes``
872+
and the ``changed_attributes`` hashes.
873+
866874

867875
Resetting Changes
868876
-----------------

lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def <<(*args)
3434
doc.save
3535
end
3636
reset_unloaded
37+
clear_foreign_key_changes
3738
end
3839
end
3940
unsynced(_base, foreign_key) and self
@@ -181,6 +182,7 @@ def substitute(replacement)
181182
push(replacement.compact.uniq)
182183
else
183184
reset_unloaded
185+
clear_foreign_key_changes
184186
end
185187
self
186188
end
@@ -198,6 +200,23 @@ def unscoped
198200

199201
private
200202

203+
# Clears the foreign key from the changed_attributes hash.
204+
#
205+
# reset_unloaded accesses the value for the foreign key on
206+
# _base, which causes it to get added to the changed_attributes
207+
# hash. This happens because when reading a "resizable" attribute
208+
# it is automatically added to the changed_attributes hash.
209+
# this is true only for the foreign key value for HABTM associations
210+
# as the other associations use strings for their foreign key values.
211+
# For consistency with the other associations, we clear the foreign
212+
# key from the changed_attributes hash.
213+
# See MONGOID-4843 for a longer discussion about this.
214+
#
215+
# @api private
216+
def clear_foreign_key_changes
217+
_base.changed_attributes.delete(foreign_key)
218+
end
219+
201220
# Appends the document to the target array, updating the index on the
202221
# document at the same time.
203222
#

0 commit comments

Comments
 (0)