How to pull one instance of an element in an array in MongoDB?

In accordance with the documents:

The $ pull operator removes from the existing array all instances of the value or values ​​that match the specified condition.

Is it possible to delete only the first instance of the value? For instance:

var array = ["bird","tiger","bird","horse"] 

How can I transfer the first “bird” directly to the update call?

+4
source share
3 answers

Thus, you are correct that the $pull operator does exactly what the documentation says, since its arguments are actually a “query” used to match elements that should be removed.

If the contents of your array always had an element in the "first" position, as you show, then the $pop operator will actually delete that first element.

With the base node driver:

 collection.findOneAndUpdate( { "array.0": "bird" }, // "array.0" is matching the value of the "first" element { "$pop": { "array": -1 } }, { "returnOriginal": false }, function(err,doc) { } ); 

Mongoose has an argument to return the changed document different:

 MyModel.findOneAndUpdate( { "array.0": "bird" }, { "$pop": { "array": -1 } }, { "new": true }, function(err,doc) { } ); 

But they are not very useful if the position of the array of the "first" element to be deleted is unknown.

For a general approach, you will need “two” updates here: one to match the first element and replace it with something unique that needs to be removed, and the second to actually delete this changed element.

This is much simpler if you apply simple updates and do not request a returned document, and can also be executed in bulk in different documents. It also helps to use something like async.series to avoid nesting your calls:

 async.series( [ function(callback) { collection.update( { "array": "bird" }, { "$unset": { "array.$": "" } }, { "multi": true } callback ); }, function(callback) { collection.update( { "array": null }, { "$pull": { "array": null } }, { "multi": true } callback ); } ], function(err) { // comes here when finished or on error } ); 

Thus, using $unset with the positional operator $ allows you to change the "first" element to null . Then a subsequent request with $pull simply removes any null entry from the array.

This way you safely remove the "first" occurrence of the value from the array. To determine if this array contains more than one identical value, this is another question.

+5
source

This is a mess that Mongo has not yet implemented this feature. However, we developed a workaround to solve this problem using a unique operator to update the document.

The trick is to maintain a subdocument that stores the values ​​you want to delete. Using your example, you will have something like the following.

 { array: ["bird","tiger","bird","horse"], toDelete: ["bird"] } 

Anytime you want to “delete” an element from an array , you add it to the toDelete array.

Obviously, you must change the way you read information from the collection. Every time you need to get the information stored in an array , you must remember to subtract toDelete elements. I think this last operation should only be done using a script in some programming language (Js, Java, etc.).

Note that once you use the script to retrieve information, you can use the values ​​of the toDelete array to remove only the first occurrences of these values ​​in the array .

This solution works in the best case when the number of subtractions to the array is relatively less than the number of additions.

Hope this helps.

0
source

It is worth noting that although the other answer here is really correct, the general approach here is to $unset element of the corresponding array to create a null value, and then $pull only null values ​​from the array, there are better ways to implement this in modern versions of MongoDB.

Using bulkWrite()

As an alternative to sending two operations for sequential updates as separate requests, the modern MongoDB release supports bulk operations using the recommended bulkWrite() method, which allows you to send these multiple updates as a single request with a single response:

  collection.bulkWrite( [ { "updateOne": { "filter": { "array": "bird" }, "update": { "$unset": { "array.$": "" } } }}, { "updateOne": { "filter": { "array": null }, "update": { "$pull": { "array": null } } }} ] ); 

Does the same as the answer, showing that there are two requests, but this time it is just one . This can save a lot of overhead when interacting with the server, so this is usually the best approach.

Using aggregation expressions

With the release of MongoDB 4.2 , aggregation expressions are now allowed in various MongoDB “update” operations. This is one step in the pipeline: $addFields , $set (which is an alias of $addFields and designed to make these update statements more logical), $project or $replaceRoot and its own alias $replaceWith . The $redact pipeline $redact also applicable to some extent. In principle, any pipeline step that returns a “modified” document is allowed.

 collection.updateOne( { "array": "horse" }, [ { "$set": { "array": { "$concatArrays": [ { "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] }, { "$slice": [ "$array", { "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] }, { "$size": "$array" } ]} ] } }} ] ); 

In this case, manipulations are used to implement the $slice and $indexOfArray , which essentially merge a new array that "skips" the first first corresponding element of the array. These abstracts are combined using the $concatArrays operator, returning a new array that is not in the first matched element.

Now this is probably more efficient since the operation, which is still a single request, is now also a single operation and will cause a slightly lesser load on the server.

Of course, the only catch is that this is not supported in any release of MongoDB prior to 4.2. bulkWrite() , on the other hand, may be a newer implementation of the API, but the actual base server calls will be applied back to MongoDB 2.6, implementing the actual "Bulk API" calls, and even revert to earlier versions. By the way, all the main drivers are on actually implement this method.

Demonstration

As a demonstration, a list of both approaches is provided:

 const { Schema } = mongoose = require('mongoose'); const uri = 'mongodb://localhost:27017/test'; const opts = { useNewUrlParser: true, useUnifiedTopology: true }; mongoose.Promise = global.Promise; mongoose.set('debug', true); mongoose.set('useCreateIndex', true); mongoose.set('useFindAndModify', false); const arrayTestSchema = new Schema({ array: [String] }); const ArrayTest = mongoose.model('ArrayTest', arrayTestSchema); const array = ["bird", "tiger", "horse", "bird", "horse"]; const log = data => console.log(JSON.stringify(data, undefined, 2)); (async function() { try { const conn = await mongoose.connect(uri, opts); await Promise.all( Object.values(conn.models).map(m => m.deleteMany()) ); await ArrayTest.create({ array }); // Use bulkWrite update await ArrayTest.bulkWrite( [ { "updateOne": { "filter": { "array": "bird" }, "update": { "$unset": { "array.$": "" } } }}, { "updateOne": { "filter": { "array": null }, "update": { "$pull": { "array": null } } }} ] ); log({ bulkWriteResult: (await ArrayTest.findOne()) }); // Use agggregation expression await ArrayTest.collection.updateOne( { "array": "horse" }, [ { "$set": { "array": { "$concatArrays": [ { "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] }, { "$slice": [ "$array", { "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] }, { "$size": "$array" } ]} ] } }} ] ); log({ aggregateWriteResult: (await ArrayTest.findOne()) }); } catch (e) { console.error(e); } finally { mongoose.disconnect(); } })(); 

And the conclusion:

 Mongoose: arraytests.deleteMany({}, {}) Mongoose: arraytests.insertOne({ array: [ 'bird', 'tiger', 'horse', 'bird', 'horse' ], _id: ObjectId("5d8f509114b61a30519e81ab"), __v: 0 }, { session: null }) Mongoose: arraytests.bulkWrite([ { updateOne: { filter: { array: 'bird' }, update: { '$unset': { 'array.$': '' } } } }, { updateOne: { filter: { array: null }, update: { '$pull': { array: null } } } } ], {}) Mongoose: arraytests.findOne({}, { projection: {} }) { "bulkWriteResult": { "array": [ "tiger", "horse", "bird", "horse" ], "_id": "5d8f509114b61a30519e81ab", "__v": 0 } } Mongoose: arraytests.updateOne({ array: 'horse' }, [ { '$set': { array: { '$concatArrays': [ { '$slice': [ '$array', 0, { '$indexOfArray': [ '$array', 'horse' ] } ] }, { '$slice': [ '$array', { '$add': [ { '$indexOfArray': [ '$array', 'horse' ] }, 1 ] }, { '$size': '$array' } ] } ] } } } ]) Mongoose: arraytests.findOne({}, { projection: {} }) { "aggregateWriteResult": { "array": [ "tiger", "bird", "horse" ], "_id": "5d8f509114b61a30519e81ab", "__v": 0 } } 

NOTE : mongoose is used in the example listing, partly because it was referenced in another answer given, and partly to demonstrate an important point in the example with generalized syntax. Please note that the code uses ArrayTest.collection.updateOne() , because in the current release of Mongoose (at the time of writing 5.7.1), the aggregation pipeline syntax for such updates was removed by the standard methods of the mongoose model.

Thus, the .collection can be used to retrieve the underlying Collection object from the main driver of the MongoDB node. This will be required until mongoose is fixed, which allows the inclusion of this expression.

0
source

All Articles