mongoose is removing empty object out of embedded documents in array - node.js

notice the following code, that shows a schema with 2 arrays, one is configured to be from Type:
[
mongoose.Schema.Types.Mixed
]
and one is configured to be from type:
[
{
value: mongoose.Schema.Types.Mixed
}
]
Here is the code:
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var schema = new mongoose.Schema({
withSchema: [{
value:mongoose.Schema.Types.Mixed}
],
withoutSchema: [mongoose.Schema.Types.Mixed],
} , {minimize: false});
var Tweak = mongoose.model('tweak', schema );
I update the document using the same data:
var data = {
"withSchema" : [ { "value": { a:"221", b:{} } } ],
"withoutSchema" : [ { "value": { a:"221", b:{} } } ]
}
Tweak.findByIdAndUpdate("545680170960023a185ea77e", data, function(err, doc){
console.log(doc);
//{
// "withSchema" : [ { "value": { a:"221" } } ],
// "withoutSchema" : [ { "value": { a:"221", b:{} } } ]
//}
});
How do I prevent this b:{} removal?
EDIT:
It turns out this happens only when there is an embeddedDocument inside an Array.

Removal of empty objects from arrays - is cause by the minimize option of schema - which defaults to 'true'. Erken answered this in a comment in the down voted answer above - putting it as a separate answer so people can find it.
Can be overridden to 'false' in the schema - then it will save empty objects in arrays
var schema = new Schema({ name: String, inventory: {} }, { minimize: false });
from http://mongoosejs.com/docs/guide.html#minimize

This approach involves first retrieving the document from Mongo, then issuing an update command (triggered by calling save). Take a look at the following code.
var id = "54619b5ef610b70b14a46e79";
Tweak.findById(id, function(err, result) {
if (err) {
console.log(err);
}
console.log(result.withSchema[0].value);
result.withSchema[0].value = data.withSchema[0].value;
result.withoutSchema.value = data.withoutSchema.value;
result.save(function(err) {
if (err) {
console.log(err);
}
console.log('updated');
});
});
After saving the document run the code snippet with appropriate 'id' value.

Objects which evaluate to null (as your b does) are skipped by mongoose. As an empty or non existant object evaluates to null in all MongoDB drivers and even shell queries, too, it actually does not make a difference wether an empty b is saved or not.
For example, if you query if b exists, since b is empty, it evaluates to nulland hence the query would fail for that document, regardless if bhas an empty value or simply isn't there.
Since you already use the minimize option, I assume that the empty result, since it evaluates to null, simply isn't displayed. When checking with the shell, the key should be there with an empty value.
Bottom line: for all practical purposes, it does not make a difference wether b holds an empty value or simply isn't there.

Related

Using updateOne method to update an object field inside array - throws error "Cannot create field 'url' in element"

I have MongoDB database (with Mongoose) containing a collection of Products (among others), which looks like this:
[
{
name: 'Product A',
url: 'product-a',
category: 'accesory',
price: 12,
shortDescription: ['example description'],
technicalSpecs: [{ speed: 10, weight: 20 }],
images: [],
reviews: [],
relatedProducts: [
{
url: 'product-b',
name: 'Product B',
// to be added in Update query
//id: id_of_related_product
}
]
} /* other Product objects */
]
As every MongoDB document is provided with _id property by default, but within the relatedProducts array i only have url and name properties, i want to add the id property (associated with corresponding Product) for each object in the relatedProducts array, so i will be able to conveniently query and process those related products.
I came up with an idea to query all Products to get only those, which have non-empty relatedProducts array. Then i loop them and i search for Product model, which has specific url and name properties - this let's me get it's true (added by MongoDB) _id. At the end i want to add this _id to matching object inside relatedProducts array.
My code:
async function assignIDsToRelatedProducts(/* Model constructor */ Product) {
const productsWithRelatedOnes = await Product.find(
{ relatedProducts: { $ne: [] }}, ['relatedProducts', 'name', 'url']
);
for (const productItem of productsWithRelatedOnes) {
for (const relatedProduct of productItem.relatedProducts) {
const product = await Product.findOne(
{ url: relatedProduct.url, name: relatedProduct.name },
'_id'
);
// throws error
await productItem.updateOne(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': product._id } }
);
}
}
}
However it throws the following error:
MongoError: Cannot create field 'url' in element {relatedProducts: [ /* array's objects here */ ]}
I don't know why MongoDB tries to create field 'url', as i use it to project/query url field (not create it) in updateOne method. How to fix this?
And - as i am newbie to MongoDB - is there a simpler way of achieving my goal? I feel that those two nested for..of loops are unnecessary, or even preceding creation of productsWithRelatedOnes variable is.
Is it possible to do with Mongoose Virtuals? I have tried it, but i couldn't match virtual property within the same Product Model - attach it to each object in relatedProducts array - after calling .execPopulate i received either an empty array or undefined (i am aware i should post at-the-time code of using Virtual, but for now i switched to above solution).
Although i didn't find solution or even reason of my problem, i solved it with a slightly other approach:
async function assignIDsToRelatedProducts(Product) {
const productsHavingRelatedProducts = Product.find({ relatedProducts: { $ne: [] }});
for await (const withRelated of productsHavingRelatedProducts) {
for (const relatedProductToUpdate of withRelated.relatedProducts) {
const relatedProduct = await Product
.findOne(
{ url: relatedProductToUpdate.url, name: relatedProductToUpdate.name },
['url', '_id']
);
await Product.updateMany(
{ 'relatedProducts.url': relatedProduct.url },
{ $set: { 'relatedProducts.$.id': relatedProduct._id } }
);
}
}
const amountOfAllProducts = await Product.find({}).countDocuments();
const amountOfRelatedProductsWithID = await Product
.find({ 'relatedProducts.id': { $exists: true } }).countDocuments();
console.log('All done?', amountOfAllProducts === amountOfRelatedProductsWithID);
}
Yet, i still suppose it can be done more concisely, without the initial looping. Hopefully somebody will suggest better solution. :)

MongoDB find an object in DB and update its field

Problem
I have a NodeJS app connecting to a MongoDB. I am tracking how many times something occurred. So, what I want is:
Check if my constructed object is in the database (excluding field with number of occurrences)
If so, update its occurrences +=1
If not, set occurrences = 1 and insert it
I have a working code:
const isInDb = await collection.findOne({
// match all other fields except for the occurrences field
});
if(!isInDb) {
parsedElement.occurrences = 1;
await collection.insertOne(parsedElement);
} else {
await collection.updateOne(isInDb, { $inc: { "occurrences": 1 } });
}
My question
Isn't there a better way? Ideally, it'd be something like collection.findAndUpdate or with upsert or something similar. What I wrote is functional, but seems inefficient to me, since I first have to query the DB for a look-up, and then query it for update.
updateOne takes a third parameter for options. Set upsert: true.
collection.updateOne({ /* match properties */ }, { $inc: { "occurrences": 1 } }, { upsert: true })
collection.updateOne({ /* match properties */ }, {
$set: parsedElement,
$inc: {
"occurrences": 1
}
}, {
upsert: true
})

How to change key Name in mongoDB [duplicate]

Assuming I have a collection in MongoDB with 5000 records, each containing something similar to:
{
"occupation":"Doctor",
"name": {
"first":"Jimmy",
"additional":"Smith"
}
Is there an easy way to rename the field "additional" to "last" in all documents? I saw the $rename operator in the documentation but I'm not really clear on how to specify a subfield.
You can use:
db.foo.update({}, {$rename:{"name.additional":"name.last"}}, false, true);
Or to just update the docs which contain the property:
db.foo.update({"name.additional": {$exists: true}}, {$rename:{"name.additional":"name.last"}}, false, true);
The false, true in the method above are: { upsert:false, multi:true }. You need the multi:true to update all your records.
Or you can use the former way:
remap = function (x) {
if (x.additional){
db.foo.update({_id:x._id}, {$set:{"name.last":x.name.additional}, $unset:{"name.additional":1}});
}
}
db.foo.find().forEach(remap);
In MongoDB 3.2 you can also use
db.students.updateMany( {}, { $rename: { "oldname": "newname" } } )
The general syntax of this is
db.collection.updateMany(filter, update, options)
https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/
You can use the $rename field update operator:
db.collection.update(
{},
{ $rename: { 'name.additional': 'name.last' } },
{ multi: true }
)
If ever you need to do the same thing with mongoid:
Model.all.rename(:old_field, :new_field)
UPDATE
There is change in the syntax in monogoid 4.0.0:
Model.all.rename(old_field: :new_field)
Anyone could potentially use this command to rename a field from the collection (By not using any _id):
dbName.collectionName.update({}, {$rename:{"oldFieldName":"newFieldName"}}, false, true);
see FYI
I am using ,Mongo 3.4.0
The $rename operator updates the name of a field and has the following form:
{$rename: { <field1>: <newName1>, <field2>: <newName2>, ... } }
for e.g
db.getCollection('user').update( { _id: 1 }, { $rename: { 'fname': 'FirstName', 'lname': 'LastName' } } )
The new field name must differ from the existing field name. To specify a in an embedded document, use dot notation.
This operation renames the field nmae to name for all documents in the collection:
db.getCollection('user').updateMany( {}, { $rename: { "add": "Address" } } )
db.getCollection('user').update({}, {$rename:{"name.first":"name.FirstName"}}, false, true);
In the method above false, true are: { upsert:false, multi:true }.To update all your records, You need the multi:true.
Rename a Field in an Embedded Document
db.getCollection('user').update( { _id: 1 }, { $rename: { "name.first": "name.fname" } } )
use link : https://docs.mongodb.com/manual/reference/operator/update/rename/
This nodejs code just do that , as #Felix Yan mentioned former way seems to work just fine , i had some issues with other snipets hope this helps.
This will rename column "oldColumnName" to be "newColumnName" of table "documents"
var MongoClient = require('mongodb').MongoClient
, assert = require('assert');
// Connection URL
//var url = 'mongodb://localhost:27017/myproject';
var url = 'mongodb://myuser:mypwd#myserver.cloud.com:portNumber/databasename';
// Use connect method to connect to the server
MongoClient.connect(url, function(err, db) {
assert.equal(null, err);
console.log("Connected successfully to server");
renameDBColumn(db, function() {
db.close();
});
});
//
// This function should be used for renaming a field for all documents
//
var renameDBColumn = function(db, callback) {
// Get the documents collection
console.log("renaming database column of table documents");
//use the former way:
remap = function (x) {
if (x.oldColumnName){
db.collection('documents').update({_id:x._id}, {$set:{"newColumnName":x.oldColumnName}, $unset:{"oldColumnName":1}});
}
}
db.collection('documents').find().forEach(remap);
console.log("db table documents remap successfully!");
}
If you are using MongoMapper, this works:
Access.collection.update( {}, { '$rename' => { 'location' => 'location_info' } }, :multi => true )

How to properly do a Bulk upsert/update in MongoDB

I'm trying to:
Find a document according to a search criteria,
If found, update some attributes
If not insert a document with some attributes.
I'm using a Bulk.unOrderedOperation as I'm also performing a single insert. And I want to do everything in one operation againast DB.
However something it's causing nothing is being inserted for the update/upsert operation.
This is the insert document:
var lineUpPointsRoundRecord = {
lineupId: lineup.id, // String
totalPoints: roundPoints, // Number
teamId: lineup.team, // String
teamName: home.team.name, // String
userId: home.iduser, // String
userName: home.user.name, // String
round: lineup.matchDate.round, // Number
date: new Date()
}
This is the upsert document:
var lineUpPointsGeneralRecord = {
teamId: lineup.team, // String
teamName: home.team.name, // String
userId: home.iduser, // String
userName: home.user.name, // String
round: 0,
signupPoints: home.signupPoints, // String
lfPoints: roundPoints+home.signupPoints, // Number
roundPoints: [roundPoints] // Number
};
This is how I'm trying to upsert/update:
var batch = collection.initializeUnorderedBulkOp();
batch.insert(lineUpPointsRoundRecord);
batch.find({team: lineUpPointsRoundRecord.teamId, round: 0}).
upsert().
update({
$setOnInsert: lineUpPointsGeneralRecord,
$inc: {lfPoints: roundPoints},
$push: {roundPoints: roundPoints}
});
batch.execute(function (err, result) {
return cb(err,result);
});
Why wouldn't it be upserting/updating?
Note
That is JS code using waterline ORM which also uses mongodb native driver.
Your syntax here is basically correct, but your general execution was wrong and you should have "seperated" the "upsert" action from the other modifications. These will otherwise "clash" and produce an error when an "upsert" occurs:
LineupPointsRecord.native(function (err,collection) {
var bulk = collection.initializeOrderedBulkOp();
// Match and update only. Do not attempt upsert
bulk.find({
"teamId": lineUpPointsGeneralRecord.teamId,
"round": 0
}).updateOne({
"$inc": { "lfPoints": roundPoints },
"$push": { "roundPoints": roundPoints }
});
// Attempt upsert with $setOnInsert only
bulk.find({
"teamId": lineUpPointsGeneralRecord.teamId,
"round": 0
}).upsert().updateOne({
"$setOnInsert": lineUpPointsGeneralRecord
});
bulk.execute(function (err,updateResult) {
sails.log.debug(err,updateResult);
});
});
Make sure your sails-mongo is a latest version supporting the Bulk operations properly be the inclusion of a recent node native driver. The most recent supports the v2 driver, which is fine for this.
I recommend use bulkWrite exemplary code with bulk upsert of many documents:
In this case you will create documents with unique md5. If document exists then will be updated but no new document is created like in classical insertMany.
const collection = context.services.get("mongodb-atlas").db("master").collection("fb_posts");
return collection.bulkWrite(
posts.map(p => {
return { updateOne:
{
filter: { md5: p.md5 },
update: {$set: p},
upsert : true
}
}
}
),
{ ordered : false }
);
https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/
Typically I have always set upsert as a property on update. Also update should be able to find the record itself so no need to find it individually.
Depending on the environment the $ may or may not be necessary.
batch.update(
{team: lineUpPointsRoundRecord.teamId, round: 0},
{
$setOnInsert: lineUpPointsGeneralRecord,
$inc: {lfPoints: roundPoints},
$push: {roundPoints: roundPoints},
$upsert: true
});

How to prevent pushing in the document with same attribute in Mongodb

I have the following structure. I would like to prevent pushing in the document with the same attribute.
E.g. Basically, i find the user object first. If i have another vid (with is already inside), it will not get pushed in. Try using $addToSet, but failed.
I am using Mongoose.
This is my Model Structure:
var User = mongoose.model('User', {
oauthID: Number,
name: String,
username: String,
email: String,
location: String,
birthday: String,
joindate: Date,
pvideos: Array
});
This is my code for pushing into Mongo
exports.pinkvideo = function(req, res) {
var vid = req.body.vid;
var oauthid = req.body.oauthid;
var User = require('../models/user.js');
var user = User.findOne({
oauthID: oauthid
}, function(err, obj) {
if (!err && obj != null) {
obj.pvideos.push({
vid: vid
});
obj.save(function(err) {
res.json({
status: 'success'
});
});
}
});
};
You want the .update() method rather than retrieving the document and using .save() after making your changes.
This not only gives you access to the $addToSet operator that was mentioned, and it's intent is to avoid duplicates in arrays it is a lot more efficient as you are only sending your changes to the database rather than the whole document back and forth:
User.update(
{ oauthID: oauthid },
{ "$addToSet": { "pVideos": vid } },
function( err, numAffected ) {
// check error
res.json({ status: "success" })
}
)
The only possible problem there is it does depend on what you are actually pushing onto the array and expecting it to be unique. So if your array already looked like this:
[ { "name": "A", "value": 1 } ]
And you sent and update with an array element like this:
{ "name": "A", "value": 2 }
Then that document would not be considered to exist purely on the value of "A" in "name" and would add an additional document rather than just replace the existing document.
So you need to be careful about what your intent is, and if this is the sort of logic you are looking for then you would need to find the document and test the existing array entries for the conditions that you want.
But for basic scenarios where you simply don't want to add a clear duplicate then $addToSet as shown is what you want.

Resources