Use mongoose / mongodb to $pull nested data from an array - node.js

say I have this array property ('articles') on a Mongoose schema:
articles: [
{
kind: 'bear',
hashtag: 'foo'
},
{
kind: 'llama',
hashtag: 'baz',
},
{
kind: 'sheep',
hashtag: 'bar',
}
]
how can I use
$pull https://docs.mongodb.org/manual/reference/operator/update/pull/
to remote objects from this array by checking the value of hashtag to see if it matches a pattern?
For example, if I want to remove an object in the articles array where hashtag='foo'.
My best guess is the following, but it doesn't seem to work:
var data = {
"$pull": {
"articles": {
"$elemMatch": {
"hashtag": "foo"
}
}
}
};
Model.update({},data); //doesn't quite work
this one seems to work:
var data = {
"$pull": {
"articles": {
"hashtag": 'foo'
}
}
};
Model.update({},data); //seems to work
if you have a better solution or if you can show an alternate solution please provide an answer thank you

The $pull operator is basically a "mini query" in itself, and as such operators like $elemMatch become irrelevant as the "query" is directly performed on each array member anyway.
As such:
Model.update(
{},
{ "$pull": { "articles": { "hashtag": "foo" }},
{ "multi": true },
function(err,numAffected) {
// handle result here
}
)
So it is looking for the matching properties within the ( correct ) specified array in all array documents, and then removing them where there is a match.
The .update() also just returns the number of affected documents, and is usually used with { "multi": true } when you expect more than one document to be updated.
If you are just looking for "one" document, or expect the modified document in response, then use .findOneAndUpdate() instead:
Model.findOneAndUpdate(
{},
{ "$pull": { "articles": { "hashtag": "foo" }},
{ "new": true },
function(err,numAffected) {
// handle result here
}
)
Though not really practical without any selection criteria, since this just updates the first document found in the collection.

Related

MongoDB: Access current value of document when adding element to array via $push

I have collection MyCollection which basically consists of its _id and a string called comment.
This collection should be bulk-updatable.
That's done like this:
for (const obj of inputObjects) {
bulkObjectsToWrite.push({
updateOne: {
filter: { _id: obj._id },
update: {
$set: {
comment: obj.comment
}
}
}
})
}
await MyCollection.bulkWrite(bulkObjectsToWrite)
So far so good.
However, now the requirement is, that a commentHistory should be added which should look like [{"oldValue": "oldValueOfComment", "newValue": "newValueOfComment"}, ...]
I know I need to use $push for adding a new object to the commentHistory array. But how do I access the comment of the document updated right now, i.e. its current value?
I've tried
$push: {
commentHistory: {
newValue: obj.comment,
oldValue: '$comment',
},
},
but to no avail. The string $comment is added hard-coded, instead of the field being accessed.
(Using Mongoose 5.12.10 and Mongo 4.4.18)
You need to use update with aggregate pipeline.
db.collection.update({
"key": 1
},
[
{
$set: {
"comment": "New",
"commentHistory": {
"$concatArrays": [ //concatenate existing history array with new array entry
"$commentHistory",
[
{
"newValue": "New",
"oldValue": "$comment" //referencing the existing value
}
]
]
}
}
}
])
Demo

mongoDB projection on array in an object

I have this document structure in the collection:
{"_id":"890138075223711744",
"guildID":"854557773990854707",
"name":"test-lab",
"game": {
"usedWords":["akşam","elma","akım"]
}
}
What is the most efficient way to get its fields except the array (it can be really large), and at the same time, see if an item exists in the array ?
I tried this:
let query = {_id: channelID}
const options = { sort: { name: 1 }, projection: { name: 1, "game.usedWords": { $elemMatch: { word}}}}
mongoClient.db(db).collection("channels").findOne(query, options);
but I got the error: "$elemMatch can not be used on nested fields"
If I've understood correctly you can use this query:
Using positional operator $ you can return only the matched word.
db.collection.find({
"game.usedWords": "akşam"
},
{
"name": 1,
"game.usedWords.$": 1
})
Example here
The output is only name and the matched word (also _id which is returned by default)
[
{
"_id": "890138075223711744",
"game": {
"usedWords": [
"akşam"
]
},
"name": "test-lab"
}
]

Push if not present or update a nested array mongoose [duplicate]

I have documents that looks something like that, with a unique index on bars.name:
{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }
. I want to either update the sub-document where { name: 'foo', 'bars.name': 'qux' } and $set: { 'bars.$.somefield': 2 }, or create a new sub-document with { name: 'qux', somefield: 2 } under { name: 'foo' }.
Is it possible to do this using a single query with upsert, or will I have to issue two separate ones?
Related: 'upsert' in an embedded document (suggests to change the schema to have the sub-document identifier as the key, but this is from two years ago and I'm wondering if there are better solutions now.)
No there isn't really a better solution to this, so perhaps with an explanation.
Suppose you have a document in place that has the structure as you show:
{
"name": "foo",
"bars": [{
"name": "qux",
"somefield": 1
}]
}
If you do an update like this
db.foo.update(
{ "name": "foo", "bars.name": "qux" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then all is fine because matching document was found. But if you change the value of "bars.name":
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then you will get a failure. The only thing that has really changed here is that in MongoDB 2.6 and above the error is a little more succinct:
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 16836,
"errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
}
})
That is better in some ways, but you really do not want to "upsert" anyway. What you want to do is add the element to the array where the "name" does not currently exist.
So what you really want is the "result" from the update attempt without the "upsert" flag to see if any documents were affected:
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } }
)
Yielding in response:
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
So when the modified documents are 0 then you know you want to issue the following update:
db.foo.update(
{ "name": "foo" },
{ "$push": { "bars": {
"name": "xyz",
"somefield": 2
}}
)
There really is no other way to do exactly what you want. As the additions to the array are not strictly a "set" type of operation, you cannot use $addToSet combined with the "bulk update" functionality there, so that you can "cascade" your update requests.
In this case it seems like you need to check the result, or otherwise accept reading the whole document and checking whether to update or insert a new array element in code.
if you dont mind changing the schema a bit and having a structure like so:
{ "name": "foo", "bars": { "qux": { "somefield": 1 },
"xyz": { "somefield": 2 },
}
}
You can perform your operations in one go.
Reiterating 'upsert' in an embedded document for completeness
I was digging for the same feature, and found that in version 4.2 or above, MongoDB provides a new feature called Update with aggregation pipeline.
This feature, if used with some other techniques, makes possible to achieve an upsert subdocument operation with a single query.
It's a very verbose query, but I believe if you know that you won't have too many records on the subCollection, it's viable. Here's an example on how to achieve this:
const documentQuery = { _id: '123' }
const subDocumentToUpsert = { name: 'xyz', id: '1' }
collection.update(documentQuery, [
{
$set: {
sub_documents: {
$cond: {
if: { $not: ['$sub_documents'] },
then: [subDocumentToUpsert],
else: {
$cond: {
if: { $in: [subDocumentToUpsert.id, '$sub_documents.id'] },
then: {
$map: {
input: '$sub_documents',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document.id', subDocumentToUpsert.id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: { $concatArrays: ['$sub_documents', [subDocumentToUpsert]] },
},
},
},
},
},
},
])
There's a way to do it in two queries - but it will still work in a bulkWrite.
This is relevant because in my case not being able to batch it is the biggest hangup. With this solution, you don't need to collect the result of the first query, which allows you to do bulk operations if you need to.
Here are the two successive queries to run for your example:
// Update subdocument if existing
collection.updateMany({
name: 'foo', 'bars.name': 'qux'
}, {
$set: {
'bars.$.somefield': 2
}
})
// Insert subdocument otherwise
collection.updateMany({
name: 'foo', $not: {'bars.name': 'qux' }
}, {
$push: {
bars: {
somefield: 2, name: 'qux'
}
}
})
This also has the added benefit of not having corrupted data / race conditions if multiple applications are writing to the database concurrently. You won't risk ending up with two bars: {somefield: 2, name: 'qux'} subdocuments in your document if two applications run the same queries at the same time.

How to get the unique results from mongoose using $setUnion and $group?

I tried something like below but dint get the expected result , This should work if the fields are not an array
// union of includes and excludes as excludesAndExcludes
getIncludesAndExcludes: (req, res)=>{
console.log('called setunion');
experienceModel.aggregate([
{ $group: {_id : {includes:"$includes", excludes:"$excludes"}}},
{ $project: { includesAndExcludes: { $setUnion: [ "$_id.includes", "$_id.excludes" ] }, _id:0 } }
], (err, data) => {
if (err) {
res.status(500).send(error);
} else {
res.json(data);
}
})
},
Most efficiently exclude the document if neither array has any element via $exists and most importantly you are going to need $ifNull to replace with a null where arrays don't exist.
experienceModel.aggregate([
// Don't include documents that have no arrays
{ "$match": {
"$or": [
{ "includes.0": { "$exists": true } },
{ "excludes.0": { "$exists": true } }
]
},
// Join the arrays and exclude the nulls
{ "$project": {
"_id": 0,
"list": {
"$setDifference": [
{ "$setUnion": [
{ "$ifNull": [ "$includes", [null] ] },
{ "$ifNull": [ "$excludes", [null] ] }
]},
[null]
]
}},
// Unwind. By earlier conditions the array must have some entries
{ "$unwind": "$list" },
// And $group on the list values as the key to produce distinct results
{ "$group": { "_id": "$list" }
],(err, data) => {
// rest of code
})
So the first $match is to filter so that at least one array must be present as a logic rule, which also possibly speeds things up. Next you join the arrays with $setUnion, being careful to replace with an array of a single element [null] using $ifNull if the field was not present. If you did not do this then any $setUnion result would be null rather than listing entries of either array. So that is quite important.
Since it is possible for the output of $setUnion to have a null item in it's list, you can remove that with $setDifference, which is the shortest form of filter you can write and works well with "sets".
All that really remains is to "de-normalize" the array in each document to single documents of each element using [$unwind][6], and we don't need new options like preserveNullAndEmptyArrays because all the logic above already took car of that. And then the final $group is simply done on those values in order to produce "unique" output, which is what the _id key in a $group statement is for.
If you want, then you can even just strip down the result from aggregate to a simple list of strings for your response using .map():
data = data.map( d => d._id );
Then all your service returns is an array of strings, and no embedded structures.
[
"Breakfast"
"Airport Pickup"
"Dinner"
"Accomodation"
]

Set field if doesn't exist, push to array, then return document

I was hoping to do this in one operation, with just hitting the database once... but I don't know if it's possible with the api's.....
what I want is to:
find the document by id(which always will exist)
add object if it doesn't already exist { dayOfYear: 3, dataStuff: [{time:
Date(arg), data: 123] }
push {time: Date(arg), data: 123] } to dataStuff array
return modified document
I cooked up something along the lines of
return this.collection.findOneAndUpdate(dataDoc,
{ $set: { dayOfYear: reqBody.dayOfYear ,
$addToSet: { dataStuff: { time: Date(reqBody.date), data: reqBody.data }
}
but no success
The update object needs "seperate" top level keys for each atomic operation:
return this.collection.findOneAndUpdate(
dataDoc,
{
"$set": { dayOfYear: reqBody.dayOfYear },
"$addToSet": {
"dataStuff": { "time": Date(reqBody.date), "data": reqBody.data }
}
},
{ "returnOriginal": false }
)
With .findOneAndUpdate() from the core API you also need to set the "returnOrginal" option to false in order to return the modified document. With the mongoose API, it is { "new": true } instead.
In this syntax, both calls are returning a "Promise" to be resolved, and not just a direct response.
If you want to check if the whole object exists, you'll have to compare all the properties, like
this.collections.update({
_id: dataDoc,
dayOfYear: {
$ne: reqBody.dayOfYear
},
dataStuff: {
$elemMatch: {
time: {
$ne: Date(arg)
},
data: {
$ne: reqBody.data
}
}
}
}, {
$set: {
dayOfYear: reqBody.dayOfYear,
},
$addToSet: {
dataStuff: {
time: Date(arg),
data: reqBody.data
}
}
});
This way, you ensure that you always update one or zero collection items. The first argument is the query that either returns no elements or a single element (because _id is there), and if it returns one element, it gets updated. Which is, I believe, exactly what you need.

Resources