How to loop through mongodb subdocument array find the matching elements and update the element fields with new values at once - mongodb

For example, if a purchase order has line items like this:
and user from the client side send us line items to be updated in an array called lineItemsToUpdate and has a format like this:
[
{ unitCost: 342,
totalQuantity: 13,
acceptedQuantity: 6,
rejectedQuantity: 18,
title: 'Unbranded Concrete Pizza',
description: 'Soft',
variant: 5f2d5eb0195026e6dd549ef0 },
{ unitCost: 189,
totalQuantity: 95,
acceptedQuantity: 49,
rejectedQuantity: 16,
title: 'Handcrafted Rubber Cheese',
description: 'Assurance',
variant: 5f2d5eaf195026e6dd549b7d },
{ unitCost: 267,
totalQuantity: 18,
acceptedQuantity: 93,
rejectedQuantity: 11,
title: 'Incredible Soft Car',
description: 'solution-oriented',
variant: 5f2d5eb0195026e6dd549d3d },
]
here in the lineItemsToUpdate array, it's possible that the value of each element has been changed by the client and I want to update all the matching element by variant field in my sub-document if I describe it in the format of a user story:
update all lineItems "fields" according to lineItemsToUpdate array where the lineItemsToUpdate.[elem].variant === linitem.[elem].variant using $set, if possible $each and arrayFilters operators
I read some example through the mongodb documentation like this one:
db.students2.update(
{ },
{ $set: { "grades.$[elem].mean" : 100 } },
{
multi: true,
arrayFilters: [ { "elem.grade": { $gte: 85 } } ]
}
)
but the problem in here { $set: { "grades.$[elem].mean" : 100 } } is that the 100 value is constant and I want this part dynamic as I described in above.

Following is JS function you have to take help of.
function(c1,arr2){
var arr1 = c1.next().lineItems;
for(i=0;i<arr1.length;i++){
for(j=0;j<arr2.length;j++){
if(arr1[i].variant===arr2[j].variant){
db.purchaseOrder.update(
{ },
{ $set: { "lineItems.$[elem].unitCost" : arr2[j].unitCost,"lineItems.$[elem].totalQuantity" : arr2[j].totalQuantity } .....},
{ multi: true,
arrayFilters: [ { "elem.variant": { $eq: arr2[j].variant } } ]
}
)
}
}
}
}
Explanation
We have to pass 2 parameters: i) Mongo cursor and ii) An array.
Cursor is the collection to be updated. For your case, it is
Purchase Order. Array is lineItemsToUpdate array here.
Next we take 2 for loops to match variant field between cursor and
array.
If there is a match, we update the Purchase Order using update
command and $set operator.
Execution
Go to your Mongo Shell. Ensure the current database has Purchase
Order collection.
Define modifyItems function as below.
var modifyItems=function(c1,arr2){
var arr1 = c1.next().lineItems;
for(i=0;i<arr1.length;i++){
for(j=0;j<arr2.length;j++){
if(arr1[i].variant===arr2[j].variant){
db.purchaseOrder.update(
{ },
{ $set: { "lineItems.$[elem].unitCost" : arr2[j].unitCost,"lineItems.$[elem].totalQuantity" : arr2[j].totalQuantity }... },
{ multi: true,
arrayFilters: [ { "elem.variant": { $eq: arr2[j].variant } } ]
}
)
}
}
}
}
Define a variable arr which contains lineItemsToUpdate.
var arr=[
{ unitCost: 342,
totalQuantity: 13,
acceptedQuantity: 6,
rejectedQuantity: 18,
title: 'Unbranded Concrete Pizza',
description: 'Soft',
variant: 5f2d5eb0195026e6dd549ef0 },
{ unitCost: 189,
totalQuantity: 95,
acceptedQuantity: 49,
rejectedQuantity: 16,
title: 'Handcrafted Rubber Cheese',
description: 'Assurance',
variant: 5f2d5eaf195026e6dd549b7d },
{ unitCost: 267,
totalQuantity: 18,
acceptedQuantity: 93,
rejectedQuantity: 11,
title: 'Incredible Soft Car',
description: 'solution-oriented',
variant: 5f2d5eb0195026e6dd549d3d }
]
Now execute the JS function.
modifyItems(db.purchaseOrder.find({},{"lineItems":1,"_id":0}),arr);
If there is no error, your Purchase Order line items will be now updated with the array values in one single shot.

Related

How to make and requests in mongodb queries

I've worked on this for about an hour now and I can't figure anything out that works so sorry if this is obvious.
I want my query to only return results where every result matches, but right now it returns a result if at least one match is found.
My document looks like this...
{
country: 'Great Britain',
data: [
{
school: 'King Alberts',
staff: [
{
name: 'John',
age: 37,
position: 'Head Teacher'
},
{
name: 'Grace',
age: 63,
position: 'Retired'
},
{
name: 'Bob',
age: 25,
position: 'Receptionist'
}
]
},
{
school: 'St Johns',
staff: [
{
name: 'Alex',
age: 22,
position: 'Head of Drama'
},
{
name: 'Grace',
age: 51,
position: 'English Teacher'
},
{
name: 'Jack',
age: 33,
position: 'Receptionist'
}
]
},
// ... more schools
]
}
The query I'm currently making looks like...
{ 'data.staff.name': { $in: names } }
and the 'names' array that is being provided looks like ['Bob', 'Grace', 'John', 'Will', 'Tom']. Currently both schools are being returned when I make this query, I think it's because the 'names' array contains 'Grace' which is a name present at both schools and so the document it matching. Does anyone know if there's a query I could make so mongodb only returns the school object if every name in the 'names' array is a member of staff at the school?
You need to use the aggregation pipeline for this, after matching the document we'll just filter out the none matching arrays, like so:
db.collection.aggregate([
{
$match: {
"data.staff.name": {
$in: names
}
}
},
{
$addFields: {
data: {
$filter: {
input: "$data",
cond: {
$eq: [
{
$size: {
"$setIntersection": [
"$$this.staff.name",
names
]
}
},
{
$size: "$$this.staff"
}
]
}
}
}
}
}
])
Mongo Playground

Upsert in nested array doesn't create parent document

Schema
{
chapter: {
required: true,
type: Schema.Types.ObjectId,
ref: "Chapter",
},
questions: {
type: [Number]
},
};
Here is an example document
{
"_id":{
"$oid":"5ff4b728b6af610f0851d2a6"
},
"chapters":[
{
"chapter":{
"$oid":"611478ab34dde61f28dbe4d3"
},
"questions":[
35,
29,
167,
180,
101,
16,
71,
23
]
},
{
"chapter":{
"$oid":"611478ac34dde61f28dbe4d8"
},
"questions":[
162
]
}
]
}
I want to "$addToSet" on "questions", such as
const someId = SOME_ID;
const chapterId = "611478ac34dde61f28dbe4d8";
const update = {
$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
},
};
await model.findOneAndUpdate(
{
_id: someId,
"chapters.chapter": chapterId,
},
update,
{ upsert: true }
)
.lean()
.exec();
This works. However, if there is no document, the "upsert" doesn't create the document.
How can I rewrite the operation so that it can update (addToSet) as well as ensure the document is created if it didn't exist?
I checked MongoDB native query use these
db.con.collection('example').updateOne(
{"chapters": {$elemMatch:{"chapter.id":ObjectId("611478ac34dde61f28dbe4d8")}}},
{$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
}},
{upsert: true})
you should find the element of array using elemMatch
{"chapters": {$elemMatch:{"chapter.id":"611478ac34dde61f28dbe4d8"}}}
I figured out, for some reason, I can't $addToSet if the parent object is not present. So I had to make one more operation.
Inspired from this Stackoverflow answer.
I fetch the "chapters" which I need to add.
From this list of fetched chapters, I check which ones exist and which ones don't.
Using the knowledge from point 2, I am using $push to add the chapters which didn't exist entirely, and "adding to set ($addToSet)" questions on the chapters which do exist.
I am posting the code which works for me.
//Data to add (which chapter?: questionNumber[])
const docId = "SOMEID";
const questionsToAdd = {
"611478ab34dde61f28dbe4d3": [1,5,6,10],
"611478ab34dde61f28dbe4d8": [5,8,20,30],
};
//Find the chapters from questionsToAdd which exist
const existingDoc = await model.findOne({
_id: docId,
chapters: { $elemMatch: { chapter: { $in: Object.keys(questionsToAdd) } } },
})
.select(["chapters.chapter"])
.lean()
.exec();
// Objectify the array of chapters
const existingChapters = (existingDoc?.chapters ?? []).map((x) => "" + x.chapter);
// Prepare what to insert, what to update
const updateObject = {
$addToSet: {},
arrayFilters: [],
$push: [],
};
for (const [index, [chapterId, questionIndices]] of Object.entries(questionsToAdd).entries()) {
if (existingChapters.includes(chapterId)) {
updateObject.$addToSet["chapters.$[filter" + index + "].questions"] = { $each: questionIndices };
updateObject.arrayFilters.push({
["filter" + index + ".chapter"]: Types.ObjectId(chapterId),
});
} else {
updateObject.$push.push({
chapter: chapterId,
questions: questionIndices,
});
}
}
if (updateObject.arrayFilters.length) {
// *Add to set the chapters which exist
await model.findOneAndUpdate(
{ _id: userId },
{
$addToSet: updateObject.$addToSet,
},
{
arrayFilters: updateObject.arrayFilters,
upsert: true,
}
)
.lean()
.exec();
}
if (updateObject.$push.length) {
// *Push the ones that does not exist
await model.findOneAndUpdate(
{ _id: userId },
{
$push: { chapters: updateObject.$push },
},
{
upsert: true,
}
)
.lean()
.exec();
}

MongoDB query - unwind and match preserving null OR different value/ add a new field based on a condition

If a have a following structure :
{
_id: 1,
name: 'a',
info: []
},
{
_id: 2,
name: 'b',
info: [
{
infoID: 100,
infoData: 'my info'
}
]
},
{
_id: 3,
name: 'c',
info: [
{
infoID: 200,
infoData: 'some info 200'
},
{
infoID: 300,
infoData: 'some info 300'
}
]
}
I need to query in such a way to obtain the documents where infoID is 100 showing the infoData, or nothing if info is empty, or contains subdocuments with infoID different from 100.
That is, I would want the following output:
{
_id: 1,
name: 'a',
infoData100: null
},
{
_id: 2,
name: 'b',
infoData100: 'my info'
},
{
_id: 3,
name: 'c',
infoData100: null
}
If I $unwind by info and $match by infoID: 100, I lose records 1 and 3.
Thanks for your responses.
Try below query :
Query :
db.collection.aggregate([
/** Adding a new field or you can use $project instead of addFields */
{
$addFields: {
infoData100: {
$cond: [
{
$in: [100, "$info.infoID"] // Check if any of objects 'info.infoID' has value 100
},
{
// If any of those has get that object & get infoData & assign it to 'infoData100' field
$let: {
vars: {
data: {
$arrayElemAt: [
{
$filter: {
input: "$info",
cond: { $eq: ["$$this.infoID", 100] }
}
},
0
]
}
},
in: "$$data.infoData"
}
},
null // If none has return NULL
]
}
}
}
]);
Test : MongoDB-Playground

How to use Insert in MongDB:s Function

Goal: use MongoDB's function to add new row by using function.
It is similar concept to add a new row by using a stored procedure in SQL Server.
Problem: the code doesn't work and what part am I missing?
{
"message" : "db.Test.myAddFunction is not a function",
"stack" : "script:1:10"
}
Info: I'm new to MongoDB
db.createCollection("Test")
db.inventory.insertMany([
{ item: "journal", qty: 25, tags: ["blank", "red"], size: { h: 14, w: 21, uom: "cm" } },
{ item: "mat", qty: 85, tags: ["gray"], size: { h: 27.9, w: 35.5, uom: "cm" } },
{ item: "mousepad", qty: 25, tags: ["gel", "blue"], size: { h: 19, w: 22.85, uom: "cm" } }
])
db.system.js.save(
{
_id : "myAddFunction" ,
value : function (x, y)
{
db.inventory.insertOne(
{ item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } }
)
return x + y;
}
}
);
db.loadServerScripts();
db.Test.myAddFunction(2, 2);
You cannot.
Once you save a function in the system.js collection, you can use the function from any JavaScript context; e.g. $where operator, mapReduce command or db.collection.mapReduce().
Source: https://docs.mongodb.com/manual/tutorial/store-javascript-function-on-server/

Validate embedded document in mongodb

I'd like to validate my documents in MongoDB. As described here, the validator query can be specified during the createCollection command.
My application is a simple todo list, so the collection list has many documents like this:
{
"name": "the list name",
"color: "red",
"items": [
{
"title": "item1",
"isDone": false,
"text": "the item description"
}
]
}
How can I sure that all documents has this shape?
EDIT: I'm using MongoDB 3.4
You can't.
There's no way to avoid new property. If this needed is required, you should consider to migrate a SQL solution.
Otherwise, your document will contain at least those members and those members will be validated.
I assume that all fields are mandatory, so no list can be added neither without a color nor having an item without a title.
I assume that color is an enum also.
{
validator: {
name: { $type: 'string', $exists: true },
color: { $in: ['red', 'green', 'blue'], $exists: true },
items: { $exists: true },
$or: [
{ items: { $size: 0 } },
{
$and: [
{ 'items.0': { $exists: true } },
{
items: {
$not: {
$elemMatch: {
$not: { $type: 'object' }
}
}
}
},
{
items: {
$not: {
$elemMatch: {
$or: [
{ title: { $not: { $type: 'string' } } },
{ title: { $not: { $exists: true } } },
{ isDone: { $not: { $type: 'bool' } } },
{ isDone: { $not: { $exists: true } } },
{ text: { $not: { $type: 'string' } } },
{ text: { $not: { $exists: true } } }
]
}
}
}
}
]
}
]
}
}
Explanation
The solution is based on the second-order logic.
Indeed, the assertion "all elements have to match A" is equal to "no element should match !A" or, if you have many queries, "all element have to match A and B" becomes "no element should match !A or !B", where ! means the negation of the following predicate.
Long explanation
The first two query items assert that all documents have name and color, the third one the items property always exists.
NB: in the third query property, there's not the $type check because mongodb treats the array type in a odd way.
The $or operation below is needed because the item property can be empty or filled with some data.
First clause of $or checks if the array is empty.
If the array is not empty, three checks should be done:
the array contains at least one element
the array has to contain only object
all object has a specified shape.
So the first $and operator element checks that at least one element is present. The second one checks that all array elements are objects. The third one asserts that all objects have a specified shape.
In mongodb, there's an operator for checking if all array elements match a query. So, using the second order logic, the check should be turned over.
In fact, the last two clauses of $and check that no elements match none of the described query. Each sub query is the negate of a wanted query.
Example
fails
{ name: 'foo' }
{ color: 'red' }
{ color: 'unknown color' }
{ name: 'foo', color: 'red' }
{ name: 'foo', color: 'red', item: 3 }
{ name: 'foo', color: 'red', item: [ 3 ] }
{ name: 'foo', color: 'red', item: [ { } ] }
{ name: 'foo', color: 'red', item: [ { title: 'ww' } ] }
{ name: 'foo', color: 'red', item: [ { title: 'ww', isDone: false } ] }
{ name: 'foo', color: 'red', item: [ { title: 44, isDone: false, text: 'the text' } ] }
pass
{ name: 'foo', color: 'red', items: [ ] },
{ name: 'foo', color: 'red', items: [ ] },
{ name: 'foo', color: 'red', items: [ { title: 'the title', isDone: false, text: 'the text' }, { title: 'the title1', isDone: true, text: 'the text' } ] }
This solution is still valid using MongoDB 3.2