I am trying to save a unique array of String elements using MongoDB but for some reason it allows me to save duplicates.
I am using mongoose. My code:
schema = mongoose.Schema({
"searchingId": { "type": String,
"unique": true,
"index": true },
"sharedTo" : {
type: [String],
unique: true,
"trim":true
}
}, {collection: 'myCollection'});
Basically the point is to keep a list of email addresses where the user had sent emails and to prevent the user from spamming them. But this schema will allow me to push any string to a sharedTo array and to .save() it no matter whether the duplicates exist. How to prevent this from happening?
EDIT:
Lahar's answer does help with my question but not entierly. I would like to prevent user from adding emails if there is at least one duplicate. So basically $addToSet will help with uniqueness but not with my question.
You can use $addToSet instead of $push to add email in "sharedTo" array.
That won't add duplicate element(email in your case).
By providing unique:true to whole array field won't check uniqueness of array element.
Please check $addToSet documentation.
you can implement your own static function that will run from the schema class to do this for every email entered
UserSchema.statics.findUseremail = function(useremail, suffix,
callback) {
var _this = this;
var possibleuseremail = useremail + (suffix || '');
_this.findOne({
useremail: possibleuseremail
}, function(err, userem) {
if (!err) {
if (!userem) {
callback(findUseremail);
} else {
return _this.findUseremail(useremail, (suffix || 0) +
1, callback);
}
} else {
callback(null);
}
});
};
So looks like I had found the solution myself but thanks to the Lahar Shah answer for pointing me in the right direction. Instead of using
Model.update(conditions, doc, [options], [callback])
I did
fetching the object
Added each of emails to my sharedTo attribute while checking for duplicates.
saving the object if no duplicates
Code:
var length = emails.length;
for( var i = 0; i < length; i++ ) {
var saved = doc.sharedTo.addToSet(emails[i]).length;
if (saved != 1) {
//status 409 - You have already sent email to user emails[i]
return;
}
}
doc[0].save(function(fail, success) {
if(fail) {
//error
} else {
//success return 200
}
});
Related
I am trying to create a service that can be used to update nested fields in a Mongoose model. In the following example I am trying to set the field 'meta.status' to the value 2. This is the service:
angular.module('rooms').factory('UpdateSvc',['$http', function($http)
{
return function(model, id, args)
{
var url = '/roomieUpdate/' + id;
$http.put(url, args).then(function(response)
{
response = response.data;
console.log(response.data);
});
}
}]);
This is how it is called in the controller:
var newStatus = {'meta.$.status' : 2};
var update = UpdateSvc("roomie", sessionStorage.getItem('userID'), newStatus);
And this is the model:
var RoomieSchema = new Schema(
{
meta:
{
created:
{
type: Date,
default: Date.now
},
status:
{
type: Number,
default: '1',
}
}
}
And this is the route:
app.put('/roomieUpdate/:id', function(req,res)
{
var id = req.params.id;
Roomie.findOneAndUpdate(
{_id: mongoose.Types.ObjectId(id)},
req.body,
{ new : true },
function(err, doc)
{
if(err)
{
console.log(err);
}
res.json(doc);
console.log(doc);
});
});
The argument is received correctly, but I can't seem to get this to work. I am not even getting an error message. console.log(doc) simply prints out the object and the field meta.status remains '1'. I have done a direct Mongo search on the target object to make sure that I wasn't just reading the old document. I've tried a great many things like separating the key and value of req.body and use {$set:{key:value}}, but result is the same.
findOneAndUpdate() by default will return the old document, not the new (updated) document.
For that, you need to set the new option:
Roomie.findOneAndUpdate({
_id : mongoose.Types.ObjectId(id)
}, req.body, { new : true }, function(err, doc) {
...
});
As it turns out, var newStatus = {'meta.$.status' : 2}; should have been var newStatus = {'meta.status' : 2}; The document now updates correctly.
The reason the $ was there in the first place was probably based on this thread:
findOneAndUpdate - Update the first object in array that has specific attribute
or another of the many threads I read through about this issue. I had tried several solutions with and without it, but couldn't get anything to go right.
I want to populate a non-id field with mongoose, and already found these questions here:
link1
link2
It turns out that mongoose didn't support this functionality now. The open issue here:https://github.com/Automattic/mongoose/issues/2562
So now, I want to populate manually with extra queries. But I have some problems in doing it
I have two schema (replies, users):
Here is the replies schema, which the author_id referencing to the user's collection's username field not _id;
var ReplySchema = new Schema({
'content' : {
type: String,
required: true
},
'author_id' : {
type: String,
ref: 'users'
},
});
When I got the entire replies. like:
replies [ {
content: 'comments one',
author_id: 'raynlon',
},
{
content: 'comments two',
author_id: 'raynlon',
} ]
FROM here, i'm doing a further step to query the author from User table, what I did is:
var replies = yield query.exec(); // which already got all the replies
var final = [];
for (var i = 0; i < replies.length; i++) {
co(function*(){
var data = yield User.Model.findOne({username: replies[i].author_id}).select('username email avatar gravatar_id name').exec();
return data;
}).then(function(value) {
final.push(value);
});
}
return final // I need to return the modified data back to the client
I always got empty [];
this is due to the async code inside for loop?
I also found some solutions, like:
Promise.all(), q.all()
these kinda parallel promise stuff....
but I don't know how to do..
the co library I used said the co() should returned as a promise, theoretically, we could then inject into Promise.all([]), but it doesn't work...so could anyone help me about this?
Ok, the problem: you created the variable final and returns it afterwars, this happens just before the Mongo-Query has executed.
Solution: You should push the Promises to final, and then resolve it all together when you reply to the client.
function queryAsync() {
var replies = yield query.exec(); // which already got all the replies
var final = [];
for (var i = 0; i < replies.length; i++) {
final.push(
User.Model.findOne({
username: replies[i].author_id
}).select('username email avatar gravatar_id name').exec()
)
}
return final; // and array with promises is returned
}
// later in your handler ..
function handler(request, response) {
// ..
// query here, and resolve promises in final here
bluebird.all(queryAsync()).then(function(final) {
// all promises resolved
response.send(final);
})
// ..
}
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.
I have a bunch of documents in a collection I need to copy and insert into the collection, changing only the parent_id on all of them. This is taking a very very long time and maxing out my CPU. This is the current implementation I have. I only need to change the parent_id on all the documents.
// find all the documents that need to be copied
models.States.find({parent_id: id, id: { $in: progress} }).exec(function (err, states) {
if (err) {
console.log(err);
throw err;
}
var insert_arr = [];
// copy every document into an array
for (var i = 0; i < states.length; i++) {
// copy with the new id
insert_arr.push({
parent_id: new_parent_id,
id: states[i].id,
// data is a pretty big object
data: states[i].data,
})
}
// batch insert
models.States.create(insert_arr, function (err) {
if (err) {
console.log(err);
throw err;
}
});
});
Here is the schema I am using
var states_schema = new Schema({
id : { type: Number, required: true },
parent_id : { type: Number, required: true },
data : { type: Schema.Types.Mixed, required: true }
});
There must be a better way to do this that I just cannot seem to come up with. Any suggestions are more than welcome! Thanks.
In such a case there is no point to do this on application layer. Just do this in database.
db.States.find({parent_id: id, id: { $in: progress} }).forEach(function(doc){
delete doc._id;
doc.parentId = 'newParentID';
db.States.insert(doc);
})
If you really need to do this in mongoose, I see the following problem:
your return all the documents that matches your criteria, then you iterate though them and copy them into another array (modifying them), then you iterate through modified elements and copy them back. So this is at least 3 times longer then what I am doing.
P.S. If you need to save to different collection, you should change db.States.insert(doc) to db.anotherColl.insert(doc)
P.S.2 If you can not do this from the shell, I hope you can find a way to insert my query into mongoose.
I've got a Schema with an array of subdocuments, I need to update just one of them. I do a findOne with the ID of the subdocument then cut down the response to just that subdocument at position 0 in the returned array.
No matter what I do, I can only get the first subdocument in the parent document to update, even when it should be the 2nd, 3rd, etc. Only the first gets updated no matter what. As far as I can tell it should be working, but I'm not a MongoDB or Mongoose expert, so I'm obviously wrong somewhere.
var template = req.params.template;
var page = req.params.page;
console.log('Template ID: ' + template);
db.Template.findOne({'pages._id': page}, {'pages.$': 1}, function (err, tmpl) {
console.log('Matched Template ID: ' + tmpl._id);
var pagePath = tmpl.pages[0].body;
if(req.body.file) {
tmpl.pages[0].background = req.body.filename;
tmpl.save(function (err, updTmpl) {
console.log(updTmpl);
if (err) console.log(err);
});
// db.Template.findOne(tmpl._id, function (err, tpl) {
// console.log('Additional Matched ID: ' + tmpl._id);
// console.log(tpl);
// tpl.pages[tmpl.pages[0].number].background = req.body.filename;
// tpl.save(function (err, updTmpl){
// if (err) console.log(err);
// });
// });
}
In the console, all of the ID's match up properly, and even when I return the updTmpl, it's saying that it's updated the proper record, even though its actually updated the first subdocument and not the one it's saying it has.
The schema just in case:
var envelopeSchema = new Schema({
background: String,
body: String
});
var pageSchema = new Schema({
background: String,
number: Number,
body: String
});
var templateSchema = new Schema({
name: { type: String, required: true, unique: true },
envelope: [envelopeSchema],
pagecount: Number,
pages: [pageSchema]
});
templateSchema.plugin(timestamps);
module.exports = mongoose.model("Template", templateSchema);
First, if you need req.body.file to be set in order for the update to execute I would recommend checking that before you run the query.
Also, is that a typo and req.body.file is supposed to be req.body.filename? I will assume it is for the example.
Additionally, and I have not done serious testing on this, but I believe your call will be more efficient if you specify your Template._id:
var template_id = req.params.template,
page_id = req.params.page;
if(req.body.filename){
db.Template.update({_id: template_id, 'pages._id': page_id},
{ $set: {'pages.$.background': req.body.filename} },
function(err, res){
if(err){
// err
} else {
// success
}
});
} else {
// return error / missing data
}
Mongoose doesn't understand documents returned with the positional projection operator. It always updates an array of subdocuments positionally, not by id. You may be interested in looking at the actual queries that mongoose is building - use mongoose.set('debug', true).
You'll have to either get the entire array, or build your own MongoDB query and go around mongoose. I would suggest the former; if pulling the entire array is going to cause performance issues, you're probably better off making each of the subdocuments a top-level document - documents that grow without bounds become problematic (at the very least because Mongo has a hard document size limit).
I'm not familiar with mongoose but the Mongo update query might be:
db.Template.update( { "pages._id": page }, { $set: { "pages.$.body" : body } } )