Skip to content

Commit 7a4c3cd

Browse files
authored
MONGOID-5363 Change #upsert to perform updating upsert rather than replacing upsert (#5492)
* MONGOID-5363 add :replace option to upsert method * MONGOID-5363 add docs, release notes for 8.1/9.0
1 parent ad832e7 commit 7a4c3cd

File tree

8 files changed

+121
-12
lines changed

8 files changed

+121
-12
lines changed

docs/reference/crud.txt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ operations with examples.
5151
has been retrieved from the database, and a* ``Hash`` *with string keys
5252
if it is a new document.*
5353

54-
*The attributes hash also contains the attributes of all embedded
55-
documents, as well as their embedded documents, etc. If an embedded
54+
*The attributes hash also contains the attributes of all embedded
55+
documents, as well as their embedded documents, etc. If an embedded
5656
association is empty, its key will not show up in the returned hash.*
5757

5858
-
@@ -224,9 +224,12 @@ operations with examples.
224224
* - ``Model#upsert``
225225

226226
*Performs a MongoDB replace with upsert on the document. If the document
227-
exists in the database, it will get overwritten with the current
228-
document in the application (any attributes present in the database but
229-
not in the application's document instance will be lost).
227+
exists in the database and the* ``:replace`` *option is set to true, it
228+
will get overwritten with the current document in the application (any
229+
attributes present in the database but not in the application's document
230+
instance will be lost). If the* ``:replace`` *option is false (default),
231+
the document will be updated, and any attributes not in the application's
232+
document will be maintained.
230233
If the document does not exist in the database, it will be inserted.
231234
Note that this only runs the* ``{before|after|around}_upsert`` *callbacks.*
232235
-
@@ -237,6 +240,7 @@ operations with examples.
237240
last_name: "Heine"
238241
)
239242
person.upsert
243+
person.upsert(replace: true)
240244

241245
* - ``Model#touch``
242246

docs/release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Release Notes
99
.. toctree::
1010
:titlesonly:
1111

12+
release-notes/mongoid-9.0
1213
release-notes/mongoid-8.1
1314
release-notes/mongoid-8.0
1415
release-notes/mongoid-7.5

docs/release-notes/mongoid-8.1.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,44 @@ An _id, a hash of conditions, or ``false``/``nil`` can now be included:
300300
Band.exists?(BSON::ObjectId('6320d96a3282a48cfce9e72c'))
301301
Band.exists?(false) # always false
302302
Band.exists?(nil) # always false
303+
304+
305+
Added ``:replace`` option to ``#upsert``
306+
----------------------------------------
307+
308+
Mongoid 8.1 adds the ``:replace`` option to the ``#upsert`` method. This option
309+
is ``false`` by default.
310+
311+
In Mongoid 8 and earlier, and in Mongoid 8.1 when passing ``replace: true``
312+
(the default) the upserted document will overwrite the current document in the
313+
database if it exists. Consider the following example:
314+
315+
.. code:: ruby
316+
317+
existing = Player.create!(name: "Juan Soto", age: 23, team: "WAS")
318+
319+
player = Player.new(name: "Juan Soto", team: "SD")
320+
player.id = existing.id
321+
player.upsert # :replace defaults to true in 8.1
322+
323+
p Player.find(existing.id)
324+
# => <Player _id: 633b42f43282a45fadfaaf9d, name: "Juan Soto", age: nil, team: "SD">
325+
326+
As you can see, the value for the ``:age`` field was dropped, because the
327+
upsert replaced the entire document instead of just updating it. If we take the
328+
same example and set ``:replace`` to ``false``, however:
329+
330+
.. code:: ruby
331+
332+
player.upsert(replace: false)
333+
334+
p Player.find(existing.id)
335+
# => <Player _id: 633b42f43282a45fadfaaf9d, name: "Juan Soto", age: 23, team: "SD">
336+
337+
This time, the value for the ``:age`` field is maintained.
338+
339+
.. note::
340+
341+
The default for the ``:replace`` option will be changed to ``false`` in
342+
Mongoid 9.0, therefore it is recommended to explicitly specify this option
343+
while using ``#upsert`` in 8.1 for easier upgradability.

docs/release-notes/mongoid-9.0.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,16 @@ the type conversion.
7373
Note that in prior Mongoid versions, typecasting Date to Time during
7474
persistence operations was already correctly using the
7575
``Mongoid.use_activesupport_time_zone`` setting.
76+
77+
78+
Flipped default for ``:replace`` option in ``#upsert``
79+
------------------------------------------------------
80+
81+
Mongoid 8.1 added the ``:replace`` option to the ``#upsert`` method. This
82+
option was used to specify whether or not the existing document should be
83+
updated or replaced.
84+
85+
Mongoid 9.0 flips the default of this flag from ``true`` => ``false``.
86+
87+
This means that, by default, Mongoid 9 will update the existing document and
88+
will not replace it.

lib/mongoid/persistable.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ def executing_atomically?
161161
# @param [ Object ] result The result of the operation.
162162
# @param [ Hash ] options The options.
163163
#
164+
# @option options [ true | false ] :validate Whether or not to validate.
165+
#
164166
# @return [ true ] true.
165167
def post_process_persist(result, options = {})
166168
post_persist unless result == false

lib/mongoid/persistable/upsertable.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,31 @@ module Upsertable
1010
# database, then Mongo will insert a new one, otherwise the fields will get
1111
# overwritten with new values on the existing document.
1212
#
13+
# If the replace option is true, unspecified attributes will be dropped,
14+
# and if it is false, unspecified attributes will be maintained. The
15+
# replace option defaults to false in Mongoid 9.
16+
#
1317
# @example Upsert the document.
1418
# document.upsert
1519
#
20+
# @example Upsert the document with replace.
21+
# document.upsert(replace: true)
22+
#
1623
# @param [ Hash ] options The validation options.
1724
#
25+
# @option options [ true | false ] :validate Whether or not to validate.
26+
# @option options [ true | false ] :replace Whether or not to replace the document on upsert.
27+
#
1828
# @return [ true ] True.
1929
def upsert(options = {})
2030
prepare_upsert(options) do
21-
collection.find(atomic_selector).replace_one(
31+
if options[:replace]
32+
collection.find(atomic_selector).replace_one(
2233
as_attributes, upsert: true, session: _session)
34+
else
35+
collection.find(atomic_selector).update_one(
36+
{ "$set" => as_attributes }, upsert: true, session: _session)
37+
end
2338
end
2439
end
2540

@@ -36,6 +51,8 @@ def upsert(options = {})
3651
#
3752
# @param [ Hash ] options The options hash.
3853
#
54+
# @option options [ true | false ] :validate Whether or not to validate.
55+
#
3956
# @return [ true | false ] If the operation succeeded.
4057
def prepare_upsert(options = {})
4158
raise Errors::ReadonlyDocument.new(self.class) if readonly? && !Mongoid.legacy_readonly

lib/mongoid/validatable.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def exit_validate
4444
#
4545
# @param [ Hash ] options The options to check.
4646
#
47+
# @option options [ true | false ] :validate Whether or not to validate.
48+
#
4749
# @return [ true | false ] If we are validating.
4850
def performing_validations?(options = {})
4951
options[:validate].nil? ? true : options[:validate]

spec/mongoid/persistable/upsertable_spec.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
end
4343
end
4444

45+
let(:options) { {} }
46+
4547
before do
46-
updated.upsert
48+
updated.upsert(options)
4749
end
4850

4951
it "updates the existing document" do
@@ -54,19 +56,46 @@
5456
expect(existing).to be_persisted
5557
end
5658

57-
context 'when existing document contains other fields' do
58-
let!(:existing) do
59-
Band.create!(name: "Photek", views: 42)
59+
shared_examples "replaces the existing fields" do
60+
it 'replaces the existing fields' do
61+
Band.count.should == 1
62+
63+
existing.reload
64+
existing.views.should be nil
65+
existing.name.should == 'Tool'
6066
end
67+
end
6168

62-
it 'removes the existing fields' do
69+
shared_examples "retains the existing fields" do
70+
it 'retains the existing fields' do
6371
Band.count.should == 1
6472

6573
existing.reload
66-
existing.views.should be nil
74+
existing.views.should eq(42)
6775
existing.name.should == 'Tool'
6876
end
6977
end
78+
79+
context 'when existing document contains other fields' do
80+
let!(:existing) do
81+
Band.create!(name: "Photek", views: 42)
82+
end
83+
84+
context "when not passing any options" do
85+
let(:options) { {} }
86+
it_behaves_like "retains the existing fields"
87+
end
88+
89+
context "when passing replace: false" do
90+
let(:options) { { replace: false } }
91+
it_behaves_like "retains the existing fields"
92+
end
93+
94+
context "when passing replace: true" do
95+
let(:options) { { replace: true } }
96+
it_behaves_like "replaces the existing fields"
97+
end
98+
end
7099
end
71100

72101
context "when no matching document exists in the db" do

0 commit comments

Comments
 (0)