MongoDB update subdocument changes its _id - node.js

I have the next document, and I want to update the addresses with id of 21, change the alias to 'Family'. I run User.update({ _id: 2, 'addresses._id': 21 }, { 'addresses.$': newAddress });
Which works fine, with an annoying side effect, is that Mongo generates a new id for the subdocument. Is there any way to update a subdocument without getting a new id?
'user': {
'_id': 2,
'addresses': [
{
'_id': '20',
'alias': 'Work',
'postal_code': 1235
},
{
'_id': '21',
'alias': 'Home',
'postal_code': 1235
}
]
}
I already solved this using
User.update(
{ _id: req.user._id, 'addresses._id': addressId },
{ $set: {
'addresses.$.alias': newAddress.alias,
'addresses.$.address': newAddress.address,
'addresses.$.areaId': newAddress.areaId,
'addresses.$.cityId': newAddress.cityId,
'addresses.$.postal_code': newAddress.postal_code
} }
);
This doesn't change the id of the subdocument, but I don't like this solution for obvious reasons.

Adding to JasonCust's answer, the correct approach is to inject the old id into the new address to keep the id unchanged and avoid having to enter each individual key.
Below is an example:
const userSchema = new Schema({
addresses: [{ country: String }]
});
const User = mongoose.model('User', userSchema);
const user = await User.create({
addresses: [
{ country: 'Egypt' }
]
});
const oldAddressId = user.addresses[0]._id;
//
const newAddress = {
_id: oldAddressId,
country: 'Japan'
};
await User.updateOne({ 'addresses._id': oldAddressId }, { 'addresses.$': newAddress });
const userAfterUpdate = await User.findOne({ _id: user._id });
assert.equal(userAfterUpdate.addresses[0].country, 'Japan');
assert.equal(userAfterUpdate.addresses[0]._id.toString(), oldAddressId.toString());

The short answer is: no. In essence the update object { 'addresses.$': newAddress } is a command to replace the entire object at the matched pointer location with the newAddress object. That said, if the newAddress object includes the _id value then it should be stored as the value.

Related

Can mongo has $cond in $push?

I need to push 1 item to array and save it to mongo if that item is not existed in array.
Sample: I have a record with an array
{
_id: ObjectId('xxxxx'),
userEmails: [
{
email: 'xxx#gmail.com,
addedAt: ISODate('xxx')
}
]
}
Current query:
db.users.updateOne(
{ _id: ObjectId('xxxxx') },
{
$push: {
userEmails: {
email: 'xxx#gmail.com',
addedAt: new Date(),
}
}
}
);
I expect if xxx#gmail.com is existed, it shouldn't pushed to array. I don't want array have duplicated items
Here We are specifying condition that if xxx#gmail.com is not inside userEmail then only we are pushing data.
db.users.updateOne(
{ _id: ObjectId('xxxxx'),"userEmails.email":{$ne: "xxx#gmail.com"} },
{
$push: {
userEmails: {
email: 'xxx#gmail.com',
addedAt: new Date(),
}
}
});
If you want to have a condition in $push, the best way to go will be to go with the $nin operation.
({name: {$nin: ["Shehroz", "virk"]}})
You can replace name with the user Email, so that it verifies the condition before updating

mongodb, check if the key exist while appending to an array

Note: checking if the key books exist or not, creating if not and than updating it.
I am using mongodb driver with nodejs.
In the db.collection('userData')The document looks like this:
{
user_id: 'user1',
books: [{
id: 'book1',
title: 'this is book1'
},
{
id: 'book1',
title: 'this is book1'
}]
}
when inserting a new book entry, how to check if the array of books exists in the document, if not then add a key books in the document and then insert the book entry.
You have to do 2 separate queries,
Find user document
Check condition if books field present
If Present then push object, else set new field
var user_id = "user1";
var bookData = { id: 'book1', title: 'this is book1' };
// FIND USER DATA
var userData = await db.collection('userData').findOne({ user_id: user_id }, { books: 1 });
var updateBody = { $push: { books: bookData } };
// IF BOOKS FIELD NOT PRESENT THEN SET NEW
if (!userData.books) {
updateBody = { $set: { books: [bookData] } };
}
var updateData = await db.collection('userData').updateOne({ user_id: user_id }, updateBody);
console.log(updateData);
Second option you can use update with aggregation pipeline starting from MongoDB 4.2,
$ifNull check is field is null then return []
$concatArrays to concat current books with new book object
var bookData = { id: 'book1', title: 'this is book1' };
db.collection('userData').update({
// put your condition
},
[{
$set: {
books: {
$concatArrays: [
{ $ifNull: ["$books", []] },
[bookData]
]
}
}
}],
{ multi: true }
);
Playground

Updating an array in a discriminated model in mongoose

I have a model called Person which has a discriminator of "Worker" which gives it an additional locations field of an array.
I am trying to push an element onto the locations array without going through the fetch/modify/save method (so I can use updateMany later on to update several documents at the same time).
Why is that not happening in the code below? I have tried this with findByIdAndUpdate and findOneAndUpdate as well.
index.js:
const { connect } = require("mongoose");
const Person = require("./Person");
connect("mongodb://127.0.0.1:27017/test?gssapiServiceName=mongodb", {
useNewUrlParser: true,
}, async () => {
console.log("Database connected")
const person = await Person.create(
{
__t: "Worker",
name: "John",
locations: ["America"],
},
)
console.log(person);
// Outputs:
// {
// locations: [ 'America' ],
// _id: 5eba279663ecdbc25d4d73d4,
// __t: 'Worker',
// name: 'John',
// __v: 0
// }
await Person.updateOne(
{ _id: person._id }
{
$push: { locations: "UK" },
},
)
const updated = await Person.findById(person._id);
console.log(updated);
// (Updating "locations" was unsuccessful)
// Outputs:
// {
// locations: [ 'America' ],
// __t: 'Worker',
// _id: 5eba279663ecdbc25d4d73d4,
// name: 'John',
// __v: 0
// }
});
Person.js:
const { Schema, model } = require("mongoose");
const personSchema = Schema({
name: String,
});
const Person = model("Person", personSchema);
Person.discriminator(
"Worker",
Schema({
locations: [String],
})
);
module.exports = Person;
So it turns out you have to pass in the key (__t) when updating from the root Parent model and not the Worker model as the database does not know what fields a Worker would have.
Therefore, you can do the following:
await Person.updateOne(
{ _id : person._id, __t: "Worker" },
{ $push: { locations: "UK" } }
)
See more in this Github issue

How to update mixed type field in Mongoose without overwriting the current data?

I have the following schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ShopSchema = new Schema({
name: Schema.Types.Mixed,
country: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
},
defaultLanguage: {
type: String
},
account: {type : Schema.ObjectId, ref : 'Account'},
});
mongoose.model('Shop', ShopSchema);
"name" field is multilingual. I mean, I will keep the multilingual data like
name: {
"en": "My Shop",
"es": "Mi Tienda"
}
My problem is, in a controller, I am using this code to update the shop:
var mongoose = require('mongoose')
var Shop = mongoose.model('Shop')
exports.update = function(req, res) {
Shop.findByIdAndUpdate(req.params.shopid, {
$set: {
name: req.body.name
}
}, function(err, shop) {
if (err) return res.json(err);
res.json(shop);
});
};
and it is obvious that new data overrides the old data. What I need is to extend the old data with the new one.
Is there any method to do that?
You should to use the method .markModified(). See the doc http://mongoosejs.com/docs/schematypes.html#mixed
Since it is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To "tell" Mongoose that the value of a Mixed type has changed, call the .markModified(path) method of the document passing the path to the Mixed type you just changed.
person.anything = { x: [3, 4, { y: "changed" }] };
person.markModified('anything');
person.save(); // anything will now get saved
Use "dot notation" for the specific element:
Shop.findByIdAndUpdate(req.params.shopid, {
"$set": {
"name.en": req.body.name
}
}, function(err, shop) {
if (err) return res.json(err);
res.json(shop);
});
});
That wil either only overwrite the "en" element if that is what you want to do or "create" a new element with the data you set it to. So if you used "de" and that did not exist there will be the other elements and a new "de" one with the value.

How to load document with a custom _id by Mongoose?

Here is my schema definition:
var DocSchema = new mongoose.Schema({
_id: {
name: String,
path: String
},
label: String,
...
});
mongoose.model('Doc', DocSchema, 'doc_parse_utf8');
var Doc = mongoose.model('Doc');
And the documents have been inserted to mongodb by other program. Then I tried to query the document:
Doc.findOne({_id:{name:name,path:path}}, function(err, doc){
if (err && err_handler) {
err_handler(err);
} else if(callback) {
callback(doc);
}
});
But, a cast error will be reported:
{ message: 'Cast to ObjectId failed for value "[object Object]" at path "_id"',
name: 'CastError',
type: 'ObjectId',
value: { name: 'mobile', path: 'etc/' },
path: '_id' }
I have searched this problem on mongoose's document, google and statckoverflow.com, however, there's no any solution for me. Please help, thanks.
All you need to do is override the _id type by setting it to Mixed.
var UserSchema = new Schema({
_id: Schema.Types.Mixed,
name: String
});
This causes Mongoose to essentially ignore the details of the object.
Now, when you use find, it will work (nearly as expected).
I'd warn you that you'll need to be certain that the order of properties on the _id object you're using must be provided in the exact same order or the _ids will not be considered to be identical.
When I tried this for example:
var User = mongoose.model('User', UserSchema);
var testId = { name: 'wiredprairie', group: 'abc'};
var u = new User({_id: testId , name: 'aaron'});
u.save(function(err, results) {
User.find().where("_id", testId)
.exec(function(err, users) {
console.log(users.length);
});
});
The console output was 0.
I noticed that the actual data in MongoDB was stored differently than I thought it had been saved:
{
"_id" : {
"group" : "abc",
"name" : "wiredprairie"
},
"name" : "aaron",
"__v" : 0
}
As you can see, it's not name then group as I'd coded. (It was alphabetical, which made sense in retrospect).
So, instead, I did this:
var User = mongoose.model('User', UserSchema);
var testId = { name: 'wiredprairie', group: 'abc'};
var u = new User({_id: testId , name: 'aaron'});
u.save(function(err, results) {
User.find().where("_id", { group: 'abc', name: 'wiredprairie'})
.exec(function(err, users) {
console.log(users.length);
});
});
Then, the console output was 1.
I think you should re-design your schema. If the database is already on service, and can not change it now, you can temporary use this to solve the problem:
mongoose.connection.on('open', function () {
mongoose.connection.db.collection('doc_parse_utf8').find({
_id: {
name: 'mobile',
path: 'etc/'
}
}).toArray(function(err, docs) {
console.log(err || docs)
})
})
As I know if you choose different order of fields in object find method will not work because
_id: {
name: 'mobile',
path: 'etc/'
}
and
_id: {
path: 'etc/',
name: 'mobile'
}
are different keys.

Resources