Mongodb: Add a field into embedded document (no array) - javascript

I'm trying to update a mongodb json document by single field. I know it would be easy if my data was in object array but beacause of reasons and uninformed early design choices it's just objects within objects.
structure of the single document in my collection:
{
"_id": 123123,
"drivers_teams": {
"drivers" : {
"4" : { <data> },
"5" : { <data indeed> },
...
},
...
}
}
I want to add new object e.g
const collection = db.get('collection');
let drivers = { };
drivers['111'] = { <new data> };
collection.update({}, {$set: { drivers_teams: { drivers } }}, { upsert: true }, function (err, doc) { ... });
But the outcome is that original objects in "drivers_teams" are wiped out and has only the new field in it.
If I try:
collection.update({}, {$set: { drivers }}, { upsert: true }, function (err, doc) { ... });
It non surprisingly inserts a new field "drivers" outside the drivers_teams
{
"_id": 123123,
"drivers" : { <new data> },
"drivers_teams": { <still original> }
}
Similar problem (+solution) here but json isn't nested like mine.
https://groups.google.com/forum/#!topic/mongodb-user/ZxdsuIU94AY
Is there a way to even accomplish what I'm trying to do? Or should I just give up and overwrite the whole document when I want to update a single field?
EDIT
Working solution here is {$set: { 'drivers_teams.drivers.111': <data> }} but what I should have mentioned is that example key '111' is actually unknown, it comes from client when it sends update data. That's why the new object was created by this solution: dynamically name mongo key field
Writing anything like { 'drivers_teams.driver.' + key: <data> } throws error.

It is given in the documentation of '$set' that to specify a field in an embedded document or in an array, you can use the dot notation. For your case you can do:
collection.update({},
{
$set: { "drivers_teams.drivers.111": { <new data> }}
},
{ upsert: true }, function (err, doc) { ... });
https://docs.mongodb.com/manual/reference/operator/update/set/
Edit:
If you are generating the field name dynamically, you can do:
const collection = db.get('collection');
let set_obj= { };
set_obj['drivers_teams.driver.' + key] = { <new data> };
collection.update({}, {$set: set_obj }})

You missed a level above the drivers sub-document. In mongo shell, I believe what you require is:
db.collection.update({}, {$set: {'drivers_teams.drivers.111': {...}}})
which will update the document to be like:
{
"_id": 123123,
"drivers_teams": {
"drivers": {
"4": {...},
"5": {...},
"111": {...}
}
}
}
Please see the Embedded Documents page for more details regarding the dot notation used by MongoDB.

Related

UpdateMany isn't updating the data

In user schema, Location is an array of objects with locations._id is ObjectId.
This is my user service file.
const updatedBy = {
_id: mongoose.Types.ObjectId(req.params.id),
"locations._id": { $in: req.body.locationIds },
};
const updatingData = { $set: { "locations.$.status": req.query.status }};
const user = await userDbServices.updateRecords(updatedBy, updatingData);
In req.body.locationIds, I'm passing an array.
And this one is the user DB service file
exports.updateRecords = async function (updateParam, updatingData) {
return userModel.updateMany(updateParam, updatingData);
};
When I hit the API, The first embedded document of location is updated. But the other ones aren't. How can I solve this?
This is actually the expected behavior of the $ identifier, from the docs:
the positional $ operator acts as a placeholder for the first element that matches the query document
To update multiple elements you want to be using the $[] identifier with arrayFilters, like so:
userModel.updateMany({
_id: mongoose.Types.ObjectId(req.params.id),
"locations._id": { $in: req.body.locationIds },
},
{
$set: {
"locations.$[elem].status": req.query.status
}
},
{
arrayFilters: [
{
"elem._id": {
$in: req.body.locationIds
}
}
]
})
Mongo Playground

Access a field with spaces in mongodb update statement

I have following document fields in a collection
{
_id: ObjectId("5f1b6b26ea1c3b378704b7e9"),
"Gene Name":"xyz"
}
I am trying to add a new field but without spaces. How can I do that ? as the following didn't work
db.getCollection('collection').updateMany(
{ },
{ $set: { "geneName": "$'Gene Name'" } }
)
And also this didn't work either
db.getCollection('collection').find({}).forEach( function(doc)
{
db.getCollection('collection').updateOne(
{_id:doc._id },
{ $set: { "geneName": doc.Gene Name } }
);
} );
Please note I do not want to RENAME the existing field. I simply want to access the current field to add it to a new field.
I will appriciate if you can provide the solution for both forEach and the aggregation pipeline (if either of these are possible for such operation) as I want to learn how to access such annoying fields using "${field}" notation as well
In MongodDB version 4.2+ you can use an aggregation pipeline in the updateOne, updateMany or update collection methods. Here is what you need:
db.collection.update({},
[
{
$set: {
"anotherfield": "$Gene Name"
}
}
])
The result will be:
{
"Gene Name": "xyz",
"_id": ObjectId("5f1b6b26ea1c3b378704b7e9"),
"anotherfield": "xyz"
}
Playground: https://mongoplayground.net/p/-kfY7sqNlBm

How to get an object from a MongoDB nested array

I have a MongoDB collection for storing chat objects with messages embedded as a nested array. The entire collection looks like this:
chats = [
{
_id: 0,
messages: [
{
_id: 0,
text: 'First message'
},
{
_id: 1,
text: 'Second message'
}
]
},
{
_id: 1,
messages: []
}
]
I would like to update a single message and return it to the user. I can update the message like this (in Node):
const chat = chats.findOneAndUpdate({
_id: ObjectId(chatID),
"messages._id": ObjectId(messageID)
}, {
$set: {
"messages.$.text": newText
}
});
The issue is that this query updates a message and returns a chat object, meaning that I have to look for an updated message in my code again. (i.e. chat.messages.find(message => message._id === messageID)).
Is there a way to get a message object from MongoDB directly? It would also be nice to do the update in the same query.
EDIT: I am using Node with mongodb.
Thank you!
Since MongoDB methods findOneAndUpdate, findAndModify do not allow to get the updated document from array field,
projection will return updated sub document of array using positional $ after array field name
returnNewDocument: true will return updated(new) document but this will return whole document object
The problem is, MongoDB cannot allowing to use a positional projection and return the new document together
For temporary solution
try using projection, this will return original document from array field using $ positional,
const chat = chats.findOneAndUpdate(
{
_id: ObjectId(chatID),
"messages._id": ObjectId(messageID)
},
{ $set: { "messages.$.text": newText } },
{
projection: { "messages.$": 1 }
}
);
Result:
{
"_id" : 0.0,
"messages" : [
{
"_id" : 0.0,
"text" : "Original Message"
}
]
}
To get an updated document, a new query could be made like this:
const message = chats.findOne({
_id: ObjectId(chatID),
"messages._id": ObjectId(messageID)
}, {
projection: {'messages.$': 1},
}).messages[0];
Result:
{
"_id": 0.0,
"text": "New message",
}

mongoose convert string collection to boolean [duplicate]

In MongoDB, is it possible to update the value of a field using the value from another field? The equivalent SQL would be something like:
UPDATE Person SET Name = FirstName + ' ' + LastName
And the MongoDB pseudo-code would be:
db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );
The best way to do this is in version 4.2+ which allows using the aggregation pipeline in the update document and the updateOne, updateMany, or update(deprecated in most if not all languages drivers) collection methods.
MongoDB 4.2+
Version 4.2 also introduced the $set pipeline stage operator, which is an alias for $addFields. I will use $set here as it maps with what we are trying to achieve.
db.collection.<update method>(
{},
[
{"$set": {"name": { "$concat": ["$firstName", " ", "$lastName"]}}}
]
)
Note that square brackets in the second argument to the method specify an aggregation pipeline instead of a plain update document because using a simple document will not work correctly.
MongoDB 3.4+
In 3.4+, you can use $addFields and the $out aggregation pipeline operators.
db.collection.aggregate(
[
{ "$addFields": {
"name": { "$concat": [ "$firstName", " ", "$lastName" ] }
}},
{ "$out": <output collection name> }
]
)
Note that this does not update your collection but instead replaces the existing collection or creates a new one. Also, for update operations that require "typecasting", you will need client-side processing, and depending on the operation, you may need to use the find() method instead of the .aggreate() method.
MongoDB 3.2 and 3.0
The way we do this is by $projecting our documents and using the $concat string aggregation operator to return the concatenated string.
You then iterate the cursor and use the $set update operator to add the new field to your documents using bulk operations for maximum efficiency.
Aggregation query:
var cursor = db.collection.aggregate([
{ "$project": {
"name": { "$concat": [ "$firstName", " ", "$lastName" ] }
}}
])
MongoDB 3.2 or newer
You need to use the bulkWrite method.
var requests = [];
cursor.forEach(document => {
requests.push( {
'updateOne': {
'filter': { '_id': document._id },
'update': { '$set': { 'name': document.name } }
}
});
if (requests.length === 500) {
//Execute per 500 operations and re-init
db.collection.bulkWrite(requests);
requests = [];
}
});
if(requests.length > 0) {
db.collection.bulkWrite(requests);
}
MongoDB 2.6 and 3.0
From this version, you need to use the now deprecated Bulk API and its associated methods.
var bulk = db.collection.initializeUnorderedBulkOp();
var count = 0;
cursor.snapshot().forEach(function(document) {
bulk.find({ '_id': document._id }).updateOne( {
'$set': { 'name': document.name }
});
count++;
if(count%500 === 0) {
// Excecute per 500 operations and re-init
bulk.execute();
bulk = db.collection.initializeUnorderedBulkOp();
}
})
// clean up queues
if(count > 0) {
bulk.execute();
}
MongoDB 2.4
cursor["result"].forEach(function(document) {
db.collection.update(
{ "_id": document._id },
{ "$set": { "name": document.name } }
);
})
You should iterate through. For your specific case:
db.person.find().snapshot().forEach(
function (elem) {
db.person.update(
{
_id: elem._id
},
{
$set: {
name: elem.firstname + ' ' + elem.lastname
}
}
);
}
);
Apparently there is a way to do this efficiently since MongoDB 3.4, see styvane's answer.
Obsolete answer below
You cannot refer to the document itself in an update (yet). You'll need to iterate through the documents and update each document using a function. See this answer for an example, or this one for server-side eval().
For a database with high activity, you may run into issues where your updates affect actively changing records and for this reason I recommend using snapshot()
db.person.find().snapshot().forEach( function (hombre) {
hombre.name = hombre.firstName + ' ' + hombre.lastName;
db.person.save(hombre);
});
http://docs.mongodb.org/manual/reference/method/cursor.snapshot/
Starting Mongo 4.2, db.collection.update() can accept an aggregation pipeline, finally allowing the update/creation of a field based on another field:
// { firstName: "Hello", lastName: "World" }
db.collection.updateMany(
{},
[{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }]
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
The first part {} is the match query, filtering which documents to update (in our case all documents).
The second part [{ $set: { name: { ... } }] is the update aggregation pipeline (note the squared brackets signifying the use of an aggregation pipeline). $set is a new aggregation operator and an alias of $addFields.
Regarding this answer, the snapshot function is deprecated in version 3.6, according to this update. So, on version 3.6 and above, it is possible to perform the operation this way:
db.person.find().forEach(
function (elem) {
db.person.update(
{
_id: elem._id
},
{
$set: {
name: elem.firstname + ' ' + elem.lastname
}
}
);
}
);
I tried the above solution but I found it unsuitable for large amounts of data. I then discovered the stream feature:
MongoClient.connect("...", function(err, db){
var c = db.collection('yourCollection');
var s = c.find({/* your query */}).stream();
s.on('data', function(doc){
c.update({_id: doc._id}, {$set: {name : doc.firstName + ' ' + doc.lastName}}, function(err, result) { /* result == true? */} }
});
s.on('end', function(){
// stream can end before all your updates do if you have a lot
})
})
update() method takes aggregation pipeline as parameter like
db.collection_name.update(
{
// Query
},
[
// Aggregation pipeline
{ "$set": { "id": "$_id" } }
],
{
// Options
"multi": true // false when a single doc has to be updated
}
)
The field can be set or unset with existing values using the aggregation pipeline.
Note: use $ with field name to specify the field which has to be read.
Here's what we came up with for copying one field to another for ~150_000 records. It took about 6 minutes, but is still significantly less resource intensive than it would have been to instantiate and iterate over the same number of ruby objects.
js_query = %({
$or : [
{
'settings.mobile_notifications' : { $exists : false },
'settings.mobile_admin_notifications' : { $exists : false }
}
]
})
js_for_each = %(function(user) {
if (!user.settings.hasOwnProperty('mobile_notifications')) {
user.settings.mobile_notifications = user.settings.email_notifications;
}
if (!user.settings.hasOwnProperty('mobile_admin_notifications')) {
user.settings.mobile_admin_notifications = user.settings.email_admin_notifications;
}
db.users.save(user);
})
js = "db.users.find(#{js_query}).forEach(#{js_for_each});"
Mongoid::Sessions.default.command('$eval' => js)
With MongoDB version 4.2+, updates are more flexible as it allows the use of aggregation pipeline in its update, updateOne and updateMany. You can now transform your documents using the aggregation operators then update without the need to explicity state the $set command (instead we use $replaceRoot: {newRoot: "$$ROOT"})
Here we use the aggregate query to extract the timestamp from MongoDB's ObjectID "_id" field and update the documents (I am not an expert in SQL but I think SQL does not provide any auto generated ObjectID that has timestamp to it, you would have to automatically create that date)
var collection = "person"
agg_query = [
{
"$addFields" : {
"_last_updated" : {
"$toDate" : "$_id"
}
}
},
{
$replaceRoot: {
newRoot: "$$ROOT"
}
}
]
db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
(I would have posted this as a comment, but couldn't)
For anyone who lands here trying to update one field using another in the document with the c# driver...
I could not figure out how to use any of the UpdateXXX methods and their associated overloads since they take an UpdateDefinition as an argument.
// we want to set Prop1 to Prop2
class Foo { public string Prop1 { get; set; } public string Prop2 { get; set;} }
void Test()
{
var update = new UpdateDefinitionBuilder<Foo>();
update.Set(x => x.Prop1, <new value; no way to get a hold of the object that I can find>)
}
As a workaround, I found that you can use the RunCommand method on an IMongoDatabase (https://docs.mongodb.com/manual/reference/command/update/#dbcmd.update).
var command = new BsonDocument
{
{ "update", "CollectionToUpdate" },
{ "updates", new BsonArray
{
new BsonDocument
{
// Any filter; here the check is if Prop1 does not exist
{ "q", new BsonDocument{ ["Prop1"] = new BsonDocument("$exists", false) }},
// set it to the value of Prop2
{ "u", new BsonArray { new BsonDocument { ["$set"] = new BsonDocument("Prop1", "$Prop2") }}},
{ "multi", true }
}
}
}
};
database.RunCommand<BsonDocument>(command);
MongoDB 4.2+ Golang
result, err := collection.UpdateMany(ctx, bson.M{},
mongo.Pipeline{
bson.D{{"$set",
bson.M{"name": bson.M{"$concat": []string{"$lastName", " ", "$firstName"}}}
}},
)

Add elements to array using $addtoset in mongodb

I am trying a sample that uses addtoset to update an array inside a collection. The new elements are being added but not as intended. According to addtoset a new element is added only if it is not in the list.
Issue:
It is simply taking whatever element is being added.
here is my code sample
Schema(mongo_database.js):
var category = new Schema({
Category_Name: { type: String, required: true},
//SubCategories: [{}]
Category_Type: { type: String},
Sub_Categories: [{Sub_Category_Name: String, UpdatedOn: { type:Date, default:Date.now} }],
CreatedDate: { type:Date, default: Date.now},
UpdatedOn: {type: Date, default: Date.now}
});
service.js
exports.addCategory = function (req, res){
//console.log(req.body);
var category_name = req.body.category_name;
var parent_category_id = req.body.parent_categoryId;
console.log(parent_category_id);
var cats = JSON.parse('{ "Sub_Category_Name":"'+category_name+'"}');
//console.log(cats);
var update = db.category.update(
{
_id: parent_category_id
},
{
$addToSet: { Sub_Categories: cats}
},
{
upsert:true
}
);
update.exec(function(err, updation){
})
}
Can someone help me to figure this out?
many thanks..
As mentioned already, $addToSet does not work this way as the elements in the array or "set" are meant to truly represent a "set" where each element is totally unique. Additionally, the operation methods such as .update() do not take the mongoose schema default or validation rules into account.
However operations such as .update() are a lot more effective than "finding" the document, then manipulating and using .save() for the changes in your client code. They also avoid concurrency problems where other processes or event operations could have modified the document after it was retrieved.
To do what you want requires making "mulitple" update statements to the server. I'ts a "fallback" logic situation where when one operation does not update the document you fallback to the the next:
models/category.js:
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var category = new Schema({
Category_Name: { type: String, required: true},
Category_Type: { type: String},
Sub_Categories: [{Sub_Category_Name: String, UpdatedOn: { type:Date, default:Date.now} }],
CreatedDate: { type:Date, default: Date.now},
UpdatedOn: {type: Date, default: Date.now}
});
exports.Category = mongoose.model( "Category", category );
in your code:
var Category = require('models/category').Category;
exports.addCategory = function(req,res) {
var category_name = req.body.category_name;
var parent_category_id = req.body.parent_categoryId;
Category.update(
{
"_id": parent_category_id,
"Sub_Categories.Sub_Category_Name": category_name
},
{
"$set": { "Sub_Categories.$.UpdatedOn": new Date() }
},
function(err,numAffected) {
if (err) throw error; // or handle
if ( numAffected == 0 )
Category.update(
{
"_id": parent_category_id,
"Sub_Categories.Sub_Category_Name": { "$ne": category_name }
},
{
"$push": {
"Sub_Categories": {
"Sub_Category_Name": category_name,
"UpdatedOn": new Date()
}
}
},
function(err,numAffected) {
if (err) throw err; // or handle
if ( numAffected == 0 )
Category.update(
{
"_id": parent_category_id
},
{
"$push": {
"Sub_Categories": {
"Sub_Category_Name": category_name,
"UpdatedOn": new Date()
}
}
},
{ "$upsert": true },
function(err,numAffected) {
if (err) throw err;
}
);
});
);
}
);
};
Essentially a possible three operations are tried:
Try to match a document where the category name exists and change the "UpdatedOn" value for the matched array element.
If that did not update. Find a document matching the parentId but where the category name is not present in the array and push a new element.
If that did not update. Perform an operation trying to match the parentId and just push the array element with the upsert set as true. Since both previous updates failed, this is basically an insert.
You can clean that up by either using something like async.waterfall to pass down the numAffected value and avoid the indentation creep, or by my personal preference of not bothering to check the affected value and just pass all statements at once to the server via the Bulk Operations API.
The latter can be accessed from a mongoose model like so:
var ObjectId = mongoose.mongo.ObjectID,
Category = require('models/category').Category;
exports.addCategory = function(req,res) {
var category_name = req.body.category_name;
var parent_category_id = req.body.parent_categoryId;
var bulk = Category.collection.initializeOrderBulkOp();
// Reversed insert
bulk.find({ "_id": { "$ne": new ObjectId( parent_category_id ) })
.upsert().updateOne({
"$setOnInsert": { "_id": new ObjectId( parent_category_id ) },
"$push": {
"Sub_Category_Name": category_name,
"UpdatedOn": new Date()
}
});
// In place
bulk.find({
"_id": new ObjectId( parent_category_id ),
"Sub_Categories.Sub_Category_Name": category_name
}).updateOne({
"$set": { "Sub_Categories.$.UpdatedOn": new Date() }
});
// Push where not matched
bulk.find({
"_id": new ObjectId( parent_category_id ),
"Sub_Categories.Sub_Category_Name": { "$ne": category_name }
}).updateOne({
"$push": {
"Sub_Category_Name": category_name,
"UpdatedOn": new Date()
}
});
// Send to server
bulk.execute(function(err,response) {
if (err) throw err; // or handle
console.log( JSON.stringify( response, undefined, 4 ) );
});
};
Note the reversed logic where the "upsert" occurs first but if course if that succeeded then only the "second" statement would apply, but actually under the Bulk API this would not affect the document. You will get a WriteResult object with the basic information similar to this (in abridged form):
{ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }
Or on the "upsert":
{
"nMatched" : 1,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("54af8fe7628bee196ce97ce0")
}
Also note the need to include the ObjectId function from the base mongo driver since this is the "raw" method from the base driver and it does not "autocast" based on schema like the mongoose methods do.
Additionally be very careful with this, because it is a base driver method and does not share the mongoose logic, so if there is no connection established to the database already then calling the .collection accessor will not return a Collection object and the subsequent method calls fail. Mongoose itself does a "lazy" instantation of the database connection, and the method calls are "queued" until the connection is available. Not so with the basic driver methods.
So it can be done, it's just that you need to handle the logic for such array handling yourself as there is no native operator to do that. But it's still pretty simple and quite efficient if you take the proper care.

Categories

Resources