in mongoDB i want to update an object depending upon object's other elements value - mongodb

I have a collection in MongoDB.
{
"_id" : ObjectId("5aaf51369d8bdfe288d1cb71"),
"companyName" : "ABC",
"name" : "BCD",
"buildInfo" : [
{
"Branch" : "IT",
"Subjects" : [
"Math",
"English",
"Computer",
]
}
],
"currentDate" : ISODate("2018-03-14T14:09:24.374Z"),
"lastModifiedBy" : "ABC.com"
}
I want to insert a new object into "buildInfo" if that branch won't be there. If the branch exists I want to update "Subjects".
I am passing Branch and Subjects to this method.
myDb.collection('ABCDEF').findAndModify(
{'name':'BCD', 'companyName': 'ABC'},
[['_id','asc']],
{
$addToSet: {
'buildInfo.$[i].Subjects': Subjects
}
},
{ upsert: true, arrayFilters: [{ 'i.Branch': Branch }] }
But it's updating if the branch is there, but it's not creating a new object if a branch is not there.

Upsert works on document level only, not for embedded documents.
The simplest way is to issue 3 consecutive updates:
myDb.collection('ABCDEF').findAndModify(
{'name':'BCD', 'companyName': 'ABC', '$or': [{'buildInfo':{'$exists': false}}, {'buildInfo': {'$not':{ '$type': 'array' }}}]},
[['_id','asc']],
{
$set: { 'buildInfo': [] }
},
{ upsert: true}
)
followed by
myDb.collection('ABCDEF').findAndModify(
{'name':'BCD', 'companyName': 'ABC', 'buildInfo.Branch': {$nin: [Branch]}},
[['_id','asc']],
{
$push: {
{ buildInfo: { Branch: Branch, Subjects: [] } }
}
}
)
followed by
myDb.collection('ABCDEF').findAndModify(
{'name':'BCD', 'companyName': 'ABC', 'buildInfo.Branch': Branch},
[['_id','asc']],
{
$addToSet: {
'buildInfo.$[].Subjects': Subject
}
}
)
The first one adds empty buildInfo array to documents that does not have one. The second update adds a subdocument with specific Branch and empty Subjects to documents that don't have the branch. The last query adds Subject to the set.

Related

Project values of different columns into one field

{
"_id" : ObjectId("5ae84dd87f5b72618ba7a669"),
"main_sub" : "MATHS",
"reporting" : [
{
"teacher" : "ABC"
}
],
"subs" : [
{
"sub" : "GEOMETRIC",
"teacher" : "XYZ",
}
]
}
{
"_id" : ObjectId("5ae84dd87f5b72618ba7a669"),
"main_sub" : "SOCIAL SCIENCE",
"reporting" : [
{
"teacher" : "XYZ"
}
],
"subs" : [
{
"sub" : "CIVIL",
"teacher" : "ABC",
}
]
}
I have simplified the structure of the documents that i have.
The basic structure is that I have a parent subject with an array of reporting teachers and an array of sub-subjects(each having a teacher)
I now want to extract all the subject(parent/sub-subjects) along with the condition if they are sub-subjects or not which are taught by a particular teacher.
For eg:
for teacher ABC i want the following structure:
[{'subject':'MATHS', 'is_parent':'True'}, {'subject':'CIVIL', 'is_parent':'FALSE'}]
-- What is the most efficient query possible ..? I have tried $project with $cond and $switch but in both the cases I have had to repeat the conditional statement for 'subject' and 'is_parent'
-- Is it advised to do the computation in a query or should I get the data dump and then modify the structure in the server code? AS in, I could $unwind and get a mapping of the parent subjects with each sub-subject and then do a for loop.
I have tried
db.collection.aggregate(
{$unwind:'$reporting'},
{$project:{
'result':{$cond:[
{$eq:['ABC', '$reporting.teacher']},
"$main_sub",
"$subs.sub"]}
}}
)
then I realised that even if i transform the else part into another query for the sub-subjects I will have to write the exact same thing for the property of is_parent
You have 2 arrays, so you need to unwind both - the reporting and the subs.
After that stage each document will have at most 1 parent teacher-subj and at most 1 sub teacher-subj pairs.
You need to unwind them again to have a single teacher-subj per document, and it's where you define whether it is parent or not.
Then you can group by teacher. No need for $conds, $filters, or $facets. E.g.:
db.collection.aggregate([
{ $unwind: "$reporting" },
{ $unwind: "$subs" },
{ $project: {
teachers: [
{ teacher: "$reporting.teacher", sub: "$main_sub", is_parent: true },
{ teacher: "$subs.teacher", sub: "$subs.sub", is_parent: false }
]
} },
{ $unwind: "$teachers" },
{ $group: {
_id: "$teachers.teacher",
subs: { $push: {
subject: "$teachers.sub",
is_parent: "$teachers.is_parent"
} }
} }
])

Mongoose / MongoDB - How to push new array element onto correct parent array element

mongodb 3.0.7
mongoose 4.1.12
I want to push new element : "bbb"
onto groups array which lives inside outer orgs array ...
original mongo data from this :
{
orgs: [
{
org: {
_bsontype: "ObjectID",
id: "123456789012"
},
groups: [
"aaa"
]
}
],
_id: {
_bsontype: "ObjectID",
id: "888888888888"
}
}
to this :
{
orgs: [
{
org: {
_bsontype: "ObjectID",
id: "123456789012"
},
groups: [
"aaa",
"bbb"
]
}
],
_id: {
_bsontype: "ObjectID",
id: "888888888888"
}
}
Here is a hardcoded solution yet I do not want
to hardcode array index (see the 0 in : 'orgs.0.groups' )
dbModel.findByIdAndUpdate(
{ _id: ObjectId("888888888888".toHex()), 'orgs.org' : ObjectId("123456789012".toHex()) },
{ $push : { 'orgs.0.groups' : 'bbb'
}
},
{ safe: true,
upsert: false,
new : true
}
)
... I was hoping a simple 'orgs.$.groups' would work, but no. Have also tried 'orgs.groups' , also no.
Do I really need to first retrieve the orgs array, identify the index
then perform some second operation to push onto proper orgs array element ?
PS - suggested duplicate answer does not address this question
Found solution, had to use
dbModel.update
not
dbModel.findOneAndUpdate nor dbModel.findByIdAndUpdate
when using '$' to indicate matched array index in multi-level documents
'orgs.$.groups'
this code works :
dbModel.update(
{ _id: ObjectId("888888888888".toHex()), 'orgs.org' : ObjectId("123456789012".toHex()) },
{ $push : { 'orgs.$.groups' : 'bbb'
}
},
{ safe: true,
upsert: false,
new : true
}
)
I wonder if this is a bug in mongoose ? Seems strange findOneAndUpdate fails to work.

Conditionally evaluate an array element to return

Considering the following data, I want to return the results of what id the default document to choose from within an Array field in MongoDB. Let's call the collection books. A sample collection data is shown below:
[
{
name: "Book1",
refs: [{ oid: "object1" }, { oid: "object2" }, {oid: "object5", default: true }]
},
{
name: "Book2",
refs: [{ oid: "object3" }, { oid: "object5", default: true }, { oid: "object7" }]
},
{
name: "Book3",
refs: [{ oid: "object4" }, { oid: "object2" }]
},
{
name: "Book4",
refs: [{ oid: "object5" }, { oid: "object4", default: true } ]
}
]
Okay. So a lot of the key values are in there for brevity but this doesn't change the point.
The desired logic here is as follows:
Find and return the document in the refs Array field that has a default of true
If there is no matching document in the array return the first document in the array
And following that logic, I would really like to see something returned as follows :
[
{
name: "Book1"
refs: [{oid: "object5", default: true }]
},
{
name: "Book2",
refs: [{ oid: "object5", default: true }]
},
{
name: "Book3",
refs: [{ oid: "object4" }]
},
{
name: "Book4",
refs: [{ oid: "object4" }]
}
]
Now I know there is the $cond operator in the aggregation pipeline, but part of this condition seems to be bound to getting a $slice on the projection where the default property does not exist on the document ( and is probably set to true but exists should suffice ).
This logic pattern rests on the expected results of using $pull to remove the element matching:
oid: "object5"
Out of each document array and then still be able to fall back to the first element of the array in a query
So I'm looking for some strong fu to be able to return the results.
And the solution cannot be to add another field in the main document referencing the value of the default field in the array document. Not having this is actually the point so the $pull operation works in a multi document update mode.
EDIT
This is intended as a query and I really mean when the default attribute is not set I want the first element in the array as it is listed. Every time.
The strings are sample data so don't rely on lexical order. All object# references are likely real $oid in the real world.
This may end up as a bounty. Schema changes are accepted within the tolerance of the update as mentioned. At worst the findings are a reasonable basis for a JIRA issue.
For reference, I launched this based out of thinking from my answer on this post, which is largely about re-thinking the schema to accommodate the goal.
Good hunting.
P.S And Webscale, people. Updates on the collection need to happen without iteration as there could be a really, really, ( oh webscale! ) big number of them.
Here's an example using the Aggregation Framework in MongoDB 2.4.9 that I think achieves the result you are after:
db.books.aggregate(
// Unwind the refs array
{ $unwind: "$refs" },
// Sort by refs.default descending so "true" values will be first, nulls last
{ $sort: {
"refs.default" : -1
}},
// Group and take the first ref; should either be "default:true" or first element
{ $group: {
_id: "$_id",
name: { $addToSet: "$name" },
refs: { $first: "$refs" }
}},
// (optional) Sort by name to match the example output
{ $sort: {
name: 1,
}},
// (optional) Clean up output
{ $project: {
_id: 0,
name: 1,
refs: 1
}}
)
Sample result:
{
"result" : [
{
"name" : [
"Book1"
],
"refs" : {
"oid" : "object5",
"default" : true
}
},
{
"name" : [
"Book2"
],
"refs" : {
"oid" : "object5",
"default" : true
}
},
{
"name" : [
"Book3"
],
"refs" : {
"oid" : "object4"
}
},
{
"name" : [
"Book4"
],
"refs" : {
"oid" : "object4",
"default" : true
}
}
],
"ok" : 1
}
Notes:
This makes an assumption on the sort order behaviour for refs where "default:true" is missing. On brief testing the original order appears to be preserved, so the "first" element of the array is as expected.
Due to the aggregation operators used, the output name is a single element array, and refs becomes an embedded object. Rather than manipulating further in the Aggregation Framework, you could just reference the correct fields in your application code.
I think the following aggregate query will work,
db.books.aggregate(
{$unwind:'$refs'},
{$group:{_id:{name:'$name',def:'$refs.default'},refs:{$first:'$refs'}}},
{$sort:{'_id.def':-1}},
{$group:{_id:'$_id.name',refs:{$first:'$refs'}}},
{$project:{name:'$_id',refs:1,_id:0}}
)
Result:
{
"result" : [
{
"refs" : {
"oid" : "object4"
},
"name" : "Book3"
},
{
"refs" : {
"oid" : "object5",
"default" : true
},
"name" : "Book1"
},
{
"refs" : {
"oid" : "object5",
"default" : true
},
"name" : "Book2"
},
{
"refs" : {
"oid" : "object4",
"default" : true
},
"name" : "Book4"
}
],
"ok" : 1
}

How to remove an array value from item in a nested document array

I want to remove "tag4" only for "user3":
{
_id: "doc"
some: "value",
users: [
{
_id: "user3",
someOther: "value",
tags: [
"tag4",
"tag2"
]
}, {
_id: "user1",
someOther: "value",
tags: [
"tag3",
"tag4"
]
}
]
},
{
...
}
Note: This collection holds items referencing many users. Users are stored in a different collection. Unique tags for each user are also stored in the users collection. If an user removes a tag (or multiple) from his account it should be deleted from all items.
I tried this query, but it removes "tag4" for all users:
{
"users._id": "user3",
"users.tags": {
$in: ["tag4"]
}
}, {
$pullAll: {
"users.$.tags": ["tag4"]
}
}, {
multi: 1
}
I tried $elemMatch (and $and) in the selector but ended up with the same result only on the first matching document or noticed some strange things happen (sometimes all tags of other users are deleted).
Any ideas how to solve this? Is there a way to "back reference" in $pull conditions?
You need to use $elemMatch in your query object so that it will only match if both the _id and tags parts match for the same element:
db.test.update({
users: {$elemMatch: {_id: "user3", tags: {$in: ["tag4"]}}}
}, {
$pullAll: {
"users.$.tags": ["tag4"]
}
}, {
multi: 1
})

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.