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",
}
Related
I want to get data from my database to work with variables.
Here is my database query:
db.progress.find({username:socket},function(err,res){
what do I get information on:
[ { _id: 61e180b54e0eea1454f8e5e6,
username: 'user',
exp: 0,
fraction: 'undf'} ]
if i send a request
db.progress.find({username:socket},{exp:1},function(err,res){
then in return I will get
[ { _id: 61e180b54e0eea1454f8e5e6, exp: 0 } ]
how do i extract only 0 from the received data. I would like to do something like this.
var findprogress = function(socket,cb){
db.progress.find({username:socket},function(err,res){
cb(res.exp,res.fraction)
})
}
findprogress(socket,function(cb){
console.log(cb.exp)//0
console.log(cb.fraction)//undf
})
but I don't know how to implement it correctly.
I recommend aggregate in this case so you can more easily manipulate your projected data. For example:
db.progress.aggregate([
{ $match: { username: socket } },
{ $project: { _id: 0, exp: 1 } }
])
This way you directly tell the query to not include the objectId, which is typically included by default.
find returns an array, so you will need to select the array element before accessing the field:
var findprogress = function(socket,cb){
db.progress.find({username:socket},function(err,res){
cb(res[0].exp,res[0].fraction)
})
}
Try to give 0 to _id:
db.progress.find({username:socket},{exp:1 , _id:0},function(err,res){
I have got a data structure:
{
field: 1,
field: 3,
field: [
{ _id: xxx , subfield: 1 },
{ _id: xxx , subfield: 1 },
]
}
I need to update a certain element in the array.
So far I can only do that by pulling out old object and pushing in a new one, but it changes the file order.
My implementation:
const product = await ProductModel.findOne({ _id: productID });
const price = product.prices.find( (price: any) => price._id == id );
if(!price) {
throw {
type: 'ProductPriceError',
code: 404,
message: `Coundn't find price with provided ID: ${id}`,
success: false,
}
}
product.prices.pull({ _id: id })
product.prices.push(Object.assign(price, payload))
await product.save()
and I wonder if there is any atomic way to implement that. Because this approach doesn't seem to be secured.
Yes, you can update a particular object in the array if you can find it.
Have a look at the positional '$' operator here.
Your current implementation using mongoose will then be somewhat like this:
await ProductModel.updateOne(
{ _id: productID, 'prices._id': id },//Finding Product with the particular price
{ $set: { 'prices.$.subField': subFieldValue } },
);
Notice the '$' symbol in prices.$.subField. MongoDB is smart enough to only update the element at the index which was found by the query.
I have a query where I first want to match find the list of matched users and then filter the matches out from the array of external users that was passed in so that I am left with users Id's that have not been matched yet.
Here is a the match Schema:
const mongoose = require('mongoose'); // only match two users at a time.
const Schema = mongoose.Schema;
const MatchSchema = new Schema({
participants: [{
type: String, ref: 'user'
}],
blocked: {
type: Boolean,
default: false
}
});
Here is the query with explanations:
db.getCollection('match').aggregate([
{
'$match': {
'$and': [
{ participants: "599f14855e9fcf95d0fe11a7" }, // the current user.
{ participants: {'$in': [ "598461fcda5afa9e0d2a8a64","598461fcda5afa9e0d111111", "599f14855e9fcf95d0fe5555"] } } // array of external users that I want to check if the current user is matched with.
]
}
},
{
'$project': {
'participants': 1
}
},
This returns the following result:
{
"_id" : ObjectId("59c0d76e66dd407f5efe7112"),
"participants" : [
"599f14855e9fcf95d0fe11a7",
"599f14855e9fcf95d0fe5555"
]
},
{
"_id" : ObjectId("59c0d76e66dd407f5efe75ac"),
"participants" : [
"598461fcda5afa9e0d2a8a64",
"599f14855e9fcf95d0fe11a7"
]
}
what I want to do next it merge the participants array form both results into one array.
Then I want to take away the those matching items from the array of external users so that I am left with user id's that have not been matched yet.
Any help would be much appreciated!
If you don't want those results in the array, then $filter them out.
Either by building the conditions with $or ( aggregation logical version ):
var currentUser = "599f14855e9fcf95d0fe11a7",
matchingUsers = [
"598461fcda5afa9e0d2a8a64",
"598461fcda5afa9e0d111111",
"599f14855e9fcf95d0fe5555"
],
combined = [currentUser, ...matchingUsers];
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$or': combined.map(c =>({ '$eq': [ c, '$$p' ] }))
}
}
}
}
}}
])
Or use $in ( again the aggregation version ) if you have MongoDB 3.4 which supports it:
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$in': ['$p', combined ]
}
}
}
}
}}
])
It really does not matter. It's just the difference of using JavaScript to build the expression before the pipeline is sent or letting a supported pipeline operator do the array comparison where it is actually supported.
Note you can also write the $match a bit more efficiently by using an "implicit" form of $and, as is shown.
Also note you have a problem in your schema definition ( but not related to this particular query ). You cannot use a "ref" to another collection as String in one collection where it is going to be ObjectId ( the default for _id, and presumed of the hex values obtained ) in the other. This mismatch means .populate() or $lookup functions cannot work. So you really should correct the types.
Unrelated to this. But something you need to fix as a priority.
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.
I've looked around quite a bit concerning this error, it seems that Mongo won't accept a . or a $ in an update, yet I still get this error
{ [MongoError: not okForStorage]
name: 'MongoError',
err: 'not okForStorage',
code: 12527,
n: 0,
connectionId: 18,
ok: 1 }
This is the object I'm updating:
{
status: "open",
type: "item",
parentId: "4fa13ba2d327ca052d000003",
_id: "4fa13bd6d327ca052d000012",
properties: {
titleInfo: [
{ title: "some item" }
]
}
}
And I'm updating it to:
{
fedoraId: 'aFedoraLib:438',
status: "closed",
type: "item",
parentId: "4fa13ba2d327ca052d000003",
_id: "4fa13bd6d327ca052d000012",
properties: {
titleInfo: [
{ title: "some item" }
]
}
}
Another possible cause I just ran into: storing an object which has periods in the string keys.
So for people getting the same error:
It's due to the fact that I was including the _id, which Mongo doesn't like apparently
I ran into this error when trying to save a JSON structure with this key-value pair (coming straight out of an AngularJS app):
"$$hashKey":"021"
Removing just that key fixed the problem. For others using Angular, it looks like calling Angular's built-in angular.toJson client-side eliminates the $$hashkey keys. From their forums:
$scope.ngObjFixHack = function(ngObj) {
var output;
output = angular.toJson(ngObj);
output = angular.fromJson(output);
return output;
}