$push in embedded document array - mongodb

I would like to mark all messages as read by 'jim'. Here is the structure of a thread:
db.threads.save({
messages: [
{
read_by: ['bob', 'jim']
},
{
read_by: ['bob']
},
{
read_by: ['bob']
}
]
})
As you can see, one message has already been read by 'jim', the rest only by 'bob'. I'd like to find and modify any embedded documents so that 'jim' is appended to the read_by array.
Here is where I got:
db.threads.findAndModify({
query: {
'messages.read_by': {
$ne: 'jim'
}
},
update: {
$push: {
'messages.$.read_by': 'jim'
}
}
})
I get this error:
uncaught exception: findAndModifyFailed failed: "can't append to array using string field name [$]"
The query works with a db.threads.find() so I guess the problem is with the update part of the findAndModify() call.

I know its been a while, but pushes into nested documents is indeed possible. You need to add an each into it. For example,
db.threads.update({
'messages.read_by': {
$ne: 'jim'
}
},
{
$push: {
'messages.read_by': {
$each: ['jim']
}
}
}
)
See here for more samples - http://docs.mongodb.org/manual/reference/operator/update/push/
Even though you are just passing only one value, for nested arrays, you need to pass $each. If the document already contains a read_by field with some values in it, then an update without the each works. Using the $each will work irrespective of whether the field exists or not.

With one operation, you can't do this yet. See this question, which is the same as yours.
You will have to this sort of operation in your application: find() all messages which jim has not already read, append him to them, and then set the messages field of your thread to this array.

Related

Is there an option to make updateMany() specify which single document failed while being updated?

I am using mongodb and i have a list of orders documents that I want to update but some of them have null values on some parameters.
If I want to update a document that has “parameter.subparameter” in null in the current object that is being updated, the updateMany() will fail in that case.
var orders = [
"ORDER_N_1",
"ORDER_N_2",
"ORDER_N_3"
]
try {
var response = db.ORDER_COLLECTION.updateMany(
{
Order_Number: { $in: orders },
},
{
$set: {
"parameter.subparameter":"1"
},
}
);
} catch(error) {
print(error)
}
print("response: "+JSON.stringify(response, null, 2))
this is the response i get from the updateMany():
response: {
"acknowledged": true,
"matchedCount": 2,
"modifiedCount": 2
}
As we can see, we have 3 objects to be updated but just only 2 were processed. All 3 objects match but as we encounter an error, the execution stopped so I want to know which document failed.
I tried looking for the documents once the updateMany() completes to see the value changed but i want an options that reduces database hits if is possible. I want to also note that I have read the UpdateMany() method documentation looking for an option to make it more "vervose" but doesn't seem to be posible
So my question is: Is there an option to make updateMany() or some other update (in bulk) method specify which document failed while being updated?

Use of positional with two queried arrays, with one not needing to affect positional operator

I am trying to push a comment to my Class document collection, which has a list of lessons. I want to make sure that the user posting the comment is registered to the Class and to do that I have a field mentors.mentorList that holds the users that have privilege to post in the Class.
Class.findOneAndUpdate(
{_id: "5e1baeb07fcee8639cbce2ac", "mentors.mentorList":{ $eq: "5c9ba636347bb645e0865283" } ,
"lessons._id": "5db221be211d7b68ac8be618" },
{ $push: { "lessons.$.comments": commentObj } },
{ new: true }
)
Which actually is using the matched index in the mentor.mentorList of the query (index 0) and updating the $push to the lessons[0].comments. Is it possible to exclude the mentors.mentorList from affecting the positional operator?

Mongoose update query - Mongoose/Mongodb

I have a collection called student, now I want to update the object value inside array based on condition. Can anyone help me to figure this logic please.
Student:
{
_id: "5996d10e0b992e5def651db4"
name: "Siva"
mark:[
{subject:"Tamil",mark:"50"},
{subject:"English",mark:"25"},
{subject:"Science",mark:"25"},
]
__v: 36
}
Expected Result:
I want to update only Tamil mark by checking the subject value...
You can update sub-array element or object by using positional operator $, in your example it should something like this:
Student.update(
{ "mark.subject": "Tamil" },
{ "$set": { "mark.$.mark": "60" } }
)
You can read more documentation here

Insert multiple documents referenced by another Schema

I have the following two schemas:
var SchemaOne = new mongoose.Schema({
id_headline: { type: String, required: true },
tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }]
});
var tagSchema = new mongoose.Schema({
_id: { type: String, required: true, index: { unique: true } }, // value
name: { type: String, required: true }
});
As you can see, in the first schema there is an array of references to the second schema.
My problem is:
Suppose that, in my backend server, I receive an array of tags (just the id's) and, before creating the SchemaOne document, I need to verify if the received tags already exist in the database and, if not, create them. Only after having all the tags stored in the database, I may assign this received array to the tags array of the to be created SchemaOne document.
I'm not sure on how to implement this? Can you give me a helping hand?
So lets assume you have input being sent to your server that essentially resolves to this:
var input = {
"id_headline": "title",
"tags": [
{ "name": "one" },
{ "name": "two" }
]
};
And as you state, you are not sure whether any of the "tags" entries alredy exists, but of course the "name" is also unique for lookup to the associated object.
What you are basically going to have to do here is "lookup" each of the elements within "tags" and return the document with the reference to use to the objects in the "Tag" model. The ideal method here is .findOneAndUpdate(), with the "upsert" option set to true. This will create the document in the collection where it is not found, and at any rate will return the document content with the reference that was created.
Note that natually, you want to ensure you have those array items resolved "first", before preceeding to saving the main "SchemaOne" object. The async library has some methods that help structure this:
async.waterfall(
[
function(callback) {
async.map(input.tags,function(tag,callback) {
Tag.findOneAndUpdate(
{ "name": tag.name },
{ "$setOnInsert": { "name": tag.name } },
{ "upsert": true, "new": true },
callback
)
},callback);
},
function(tags,callback) {
Model.findOneAndUpdate(
{ "id_headline": input.id_headline },
{ "$addToSet": {
"tags": { "$each": tags.map(function(tag) { return tag._id }) }
}},
{ "upsert": true, "new": true },
callback
)
}
],
function(err,result) {
// if err then do something to report it, otherwise it's done.
}
)
So the async.waterfall is a special flow control method that will pass the result returned from each of the functions specified in the array of arguments to the next one, right until the end of execution where you can optionally pass in the result of the final function in the list. It basically "cascades" or "waterfalls" results down to each step. This is wanted to pass in the results of the "tags" creation to the main model creation/modification.
The async.map within the first executed stage looks at each of the elements within the array of the input. So for each item contained in "tags", the .findOneAndUpdate() method is called to look for and possibly create if not found, the specified "tag" entry in the collection.
Since the output of .map() is going to be an array of those documents, it is simply passed through to the next stage. Therefore each iteration returns a document, when the iteration is complete you have all documents.
The next usage of .findOneAndUpdate() with "upsert" is optional, and of course considers that the document with the matching "id_headline" may or may not exist. The same case is true that if it is there then the "update" is processed, if not then it is simply created. You could optionally .insert() or .create() if the document was known not to be there, but the "update" action gives some interesting options.
Namely here is the usage of $addToSet, where if the document already existed then the specified items would be "added" to any content that was already there, and of course as a "set", any items already present would not be new additions. Note that only the _id fields are required here when adding to the array with an atomic operator, hence the .map() function employed.
An alternate case on "updating" could be to simply "replace" the array content using the $set atomic operation if it was the intent to only store those items that were mentioned in the input and no others.
In a similar manner the $setOnInsert shown when "creating"/"looking for" items in "Tags" makes sure that there is only actual "modification" when the object is "created/inserted", and that removes some write overhead on the server.
So the basic priciples of using .findOneAndUpdate() at least for the "Tags" entries is the most optimal way of handling this. This avoids double handling such as:
Querying to see if the document exists by name
if No result is returned, then send an additional statement to create one
That means two operations to the database with communication back and forth, which the actions here using "upserts" simplifies into a single request for each item.

Removing duplicate records using MapReduce

I'm using MongoDB and need to remove duplicate records. I have a listing collection that looks like so: (simplified)
[
{ "MlsId": "12345"" },
{ "MlsId": "12345" },
{ "MlsId": "23456" },
{ "MlsId": "23456" },
{ "MlsId": "0" },
{ "MlsId": "0" },
{ "MlsId": "" },
{ "MlsId": "" }
]
A listing is a duplicate if the MlsId is not "" or "0" and another listing has that same MlsId. So in the example above, the 2nd and 4th records would need to be removed.
How would I find all duplicate listings and remove them? I started looking at MapReduce but couldn't find an example that fit my case.
Here is what I have so far, but it doesn't check if the MlsId is "0" or "":
m = function () {
emit(this.MlsId, 1);
}
r = function (k, vals) {
return Array.sum(vals);
}
res = db.Listing.mapReduce(m,r);
db[res.result].find({value: {$gt: 1}});
db[res.result].drop();
I have not used mongoDB but I have used mapreduce. I think you are on the right track in terms of the mapreduce functions. To exclude he 0 and empty strings, you can add a check in the map function itself.. something like
m = function () {
if(this.MlsId!=0 && this.MlsId!="") {
emit(this.MlsId, 1);
}
}
And reduce should return key-value pairs. So it should be:
r = function(k, vals) {
emit(k,Arrays.sum(vals);
}
After this, you should have a set of key-value pairs in output such that the key is MlsId and the value is the number of thimes this particular ID occurs. I am not sure about the db.drop() part. As you pointed out, it will most probably delete all MlsIds instead of removing only the duplicate ones. To get around this, maybe you can call drop() first and then recreate the MlsId once. Will that work for you?
In mongodb you can use a query to restrict documents that are passed in for mapping. You probably want to do that for the ones you don't care about. Then in the reduce function you can ignore the dups and only return one of the docs for each duplicate key.
I'm a little confused about your goal though. If you just want to find duplicates and remove all but one of them then you can just create a unique index on that field and use the dropDups option; the process of creating the index will drop duplicate docs. Keeping the index will ensure that it doesn't happen again.
http://www.mongodb.org/display/DOCS/Indexes#Indexes-DuplicateValues
You can use aggregation operation to remove duplicates. Unwind, introduce a dummy $group and $sum stage and ignore the counts in your next stage. Something like this,
db.myCollection.aggregate([
{
$unwind: '$list'
},
{
$group:{
'_id':
{
'listing_id':'$_id', 'MlsId':'$list.MlsId'
},
'count':
{
'$sum':1
}
}
},
{
$group:
{
'_id':'$_id.listing_id',
'list':
{
'$addToSet':
{
'MlsId':'$_id.MlsId'
}
}
}
}
]);
this is how I following the #harri answer to remove duplicates:
//contains duplicated documents id and numeber of duplicates
db.createCollection("myDupesCollection")
res = db.sampledDB.mapReduce(m, r, { out : "myDupesCollection" });
// iterate through duplicated docs and remove duplicates (keep one)
db.myDupesCollection.find({value: {$gt: 1}}).forEach(function(myDoc){
u_id = myDoc._id.MlsId;
counts =myDoc.value;
db.sampledDB.remove({MlsId: u_id},counts-1); //if there are 3 docs, remove 3-1=2 of them
});