Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mongodb $addToSet to a non-array field when update on upsert

Tags:

mongodb

My recent project encountered the same problem as this one: the question

db.test.update(
    {name:"abc123", "config.a":1  }, 
    {$addToSet:{ config:{a:1,b:2} } }, 
    true 
)

Will produce such error:

Cannot apply $addToSet to a non-array field

But after changed to:

db.test.update(
    {name:"abc123", "config.a":{$in:[1]}  }, 
    {$addToSet:{ config:{a:1,b:2} } }, 
    true 
)

It works fine.

Also referenced this link: Answer

Can Any one explain what's going on? "config.a":1 will turn config to be an object? Where "config.a":{$in:[1]} won't?

like image 673
James Yang Avatar asked Mar 20 '15 14:03

James Yang


People also ask

What is addToSet in MongoDB?

The $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array. The $addToSet operator has the form: { $addToSet: { <field1>: <value1>, ... } } To specify a <field> in an embedded document or in an array, use dot notation.

How do I update an array in MongoDB?

You can use the updateOne() or updateMany() methods to add, update, or remove array elements based on the specified criteria. It is recommended to use the updateMany() method to update multiple arrays in a collection.

How do I push an array object in MongoDB?

In MongoDB, the $push operator is used to appends a specified value to an array. If the mentioned field is absent in the document to update, the $push operator add it as a new field and includes mentioned value as its element. If the updating field is not an array type field the operation failed.


2 Answers

What you are trying to do here is add a new item to an array only where the item does not exist and also create a new document where it does not exist. You choose $addToSet because you want the items to be unique, but in fact you really want them to be unique by "a" only.

So $addToset will not do that, and you rather need to "test" the element being present. But the real problem here is that it is not possible to both do that and "upsert" at the same time. The logic cannot work as a new document will be created whenever the array element was not found, rather than append to the array element like you want.

The current operation errors by design as $addToSet cannot be used to "create" an array, but only to "add" members to an existing array. But as stated already, you have other problems with achieving the logic.

What you need here is a sequence of update operations that each "try" to perform their expected action. This can only be done with multiple statements:

// attempt "upsert" where document does not exist
// do not alter the document if this is an update
db.test.update(
    { "name": "abc" },
    { "$setOnInsert": { "config": [{ "a": 1, "b": 2 }] }},
    { "upsert": true }
)

// $push the element where "a": 1 does not exist
db.test.update(
    { "name": "abc", "config.a": { "$ne": 1 } },
    { "$push": { "config": { "a": 1, "b": 2 } }}
)

// $set the element where "a": 1 does exist
db.test.update(
    { "name": "abc", "config.a": 1 },
    { "$set": { "config.$.b": 2 } }
)

On a first iteration the first statement will "upsert" the document and create the array with items. The second statement will not match the document because the "a" element has the value that was specified. The third statement will match the document but it will not alter it in a write operation because the values have not changed.

If you now change the input to "b": 3 you get different responses but the desired result:

db.test.update(
    { "name": "abc" },
    { "$setOnInsert": { "config": [{ "a": 1, "b": 3 }] }},
    { "upsert": true }
)

db.test.update(
    { "name": "abc", "config.a": { "$ne": 1 } },
    { "$push": { "config": { "a": 1, "b": 3 } }}
)

db.test.update(
    { "name": "abc", "config.a": 1 },
    { "$set": { "config.$.b": 3 } }
)

So now the first statement matches a document with "name": "abc" but does not do anything since the only valid operations are on "insert". The second statement does not match because "a" matches the condition. The third statment matches the value of "a" and changes "b" in the matched element to the desired value.

Subsequently changing "a" to another value that does not exist in the array allows both 1 and 3 to do nothing but the second statement adds another member to the array keeping the content unique by their "a" keys.

Also submitting a statement with no changes from existing data will of course result in a response that says nothing is changed on all accounts.

That's how you do your operations. You can do this with "ordered" Bulk operations so that there is only a single request and response from the server with the valid response to modified or created.

like image 134
Neil Lunn Avatar answered Sep 20 '22 03:09

Neil Lunn


As explained in this issue on the MongoDB JIRA (https://jira.mongodb.org/browse/SERVER-3946), this can be solved in a single query:

The following update fails because we use $addToSet on a field which we have also included in the first argument (the field which accepts the fields and values to query for). As far as I understand it, you can't use upsert: true in this scenario where we $addToSet to the same field we query with.

db.foo.update({x : "a"},  {$addToSet: {x: "b"}} , {upsert: true}); // FAILS

The solution is to use $elemMatch: {$eq: field: value}

db.foo.update({x: {$elemMatch: {$eq: "a"}}}, {$addToSet: {x: "b"}}, {upsert: true});
like image 35
Mikhail Avatar answered Sep 23 '22 03:09

Mikhail