MongoDB update array in multiple nested document - mongodb

I have a collections like the following
{
_id: 1
rooms:[{
_id: 1,
doors: [{
_id: 1,
name: 'kitchen',
color: 'green'
},{
_id:2,
name: 'bathroom',
color: 'red'
}]
}]
}
I want to update the whole content of specific door by id.
I try this code but it not work.
db.collections.update({_id:1, "rooms._id":1, "rooms.doors._id":2}, {$set: {rooms.$.doors.$: {_id:2, name: 'balcony', color:'yellow'}}}
If I use "rooms.$.doors.0", it will always update the first door in array.
db.collections.update({_id:1, "rooms._id":1, "rooms.doors._id":2}, {$set: {rooms.$.doors.0: {_id:2, name: 'balcony', color:'yellow'}}}
Anyone can help?.
Thank you for your looking.

you should try to avoid nesting multiple array like that. it's too troublesome if you wish to update. In previous version of mongodb, this guide still works, apparently not in latest version of mongodb. Just for fun, if you read the guide, imagine, how many $$ we need to update another nested array.
you can try aggregation, but I think it is easier to bring the the whole document with {_id:1} to server, replace the whole rooms_array and save it.
db.collections.find({_id:1}).exec((err, house)=>{
var rooms = house.rooms.map((r)=> {
return r.doors.map((d) => {
if(r._id === 1 && d._id === 2){
return {_id:2, name: 'balcony', color:'yellow'}
}
else{
return d
}
})
})
house.rooms = rooms
house.save()
})
alternative, perhaps you can do object reference for doors. Instead another document, just include ObjectId that refers to the doors. This door will be another new collection.

Related

Meteor/Mongo, push into an array inside an object inside an array

I am trying to teach myself Meteor and Mongo. I have a particular insertion inside my Meteor method that is driving me nuts.
My document object looks like this:
{
_id
name: "name",
stuff: {},
array: [
{
id: 0,
target:[
{
id: 0,
name: "1"
},{
id: 1,
name: "2"
}
]
},{
id: 1,
target:[
{
id: 0,
name: "A"
},{
id: 1,
name: "B"
}
]
}
],
}
I am trying to add objects into the target array, which is inside an object inside the array-array.
I have tried a number of different approaches over a few days based on some things I have seen here on stack overflow. The most recent attempt is:
Documents.update({_id: id, 'array.id': arrayId}, {$addToSet:{'array.$.target': objectToInsert}},{upsert: false, multi: false})
If anyone could point me in the right direction I would appreciate it.
I donot know the mongo query to do something like this but it can be done as below
let theArray=Document.findOne({_id:id}).array,
arrayOfIds=_.pluck(theArray,"id"),
index=_.indexOf(arrayOfIds,arrayId),
theArray[index].target.push(objectTobeInserted)
Now update the document with the modified array
Document.update({_id:id},{$set:{array:theArray})
If you donot understand _.pluck and _.indexOf, you can refer to underscorejs
Seems this could be an issue with where you are doing the update. If this is on the client, then see here:
Update an subdocument contained in an array contained in a MongoDB document
Solution is to move this to the server.
Also, you may want to look at the difference between $addToSet and $push here MongoDb: Difference between $push/$addtoset
If this is not on the client side and changing to push does not fix your issue, can you add to your question what the error is.

Meteor collection get last document of each selection

Currently I use the following find query to get the latest document of a certain ID
Conditions.find({
caveId: caveId
},
{
sort: {diveDate:-1},
limit: 1,
fields: {caveId: 1, "visibility.visibility":1, diveDate: 1}
});
How can I use the same using multiple ids with $in for example
I tried it with the following query. The problem is that it will limit the documents to 1 for all the found caveIds. But it should set the limit for each different caveId.
Conditions.find({
caveId: {$in: caveIds}
},
{
sort: {diveDate:-1},
limit: 1,
fields: {caveId: 1, "visibility.visibility":1, diveDate: 1}
});
One solution I came up with is using the aggregate functionality.
var conditionIds = Conditions.aggregate(
[
{"$match": { caveId: {"$in": caveIds}}},
{
$group:
{
_id: "$caveId",
conditionId: {$last: "$_id"},
diveDate: { $last: "$diveDate" }
}
}
]
).map(function(child) { return child.conditionId});
var conditions = Conditions.find({
_id: {$in: conditionIds}
},
{
fields: {caveId: 1, "visibility.visibility":1, diveDate: 1}
});
You don't want to use $in here as noted. You could solve this problem by looping through the caveIds and running the query on each caveId individually.
you're basically looking at a join query here: you need all caveIds and then lookup last for each.
This is a problem of database schema/denormalization in my opinion: (but this is only an opinion!):
You could as mentioned here, lookup all caveIds and then run the single query for each, every single time you need to look up last dives.
However I think you are much better off recording/updating the last dive inside your cave document, and then lookup all caveIds of interest pulling only the lastDive field.
That will give you immediately what you need, rather than going through expensive search/sort queries. This is at the expense of maintaining that field in the document, but it sounds like it should be fairly trivial as you only need to update the one field when a new event occurs.

Publish all fields in document but just part of an array in the document

I have a mongo collection in which the documents have a field that is an array. I want to be able to publish everything in the documents except for the elements in the array that were created more than a day ago. I suspect the answer will be somewhat similar to this question.
Meteor publication: Hiding certain fields in an array document field?
Instead of limiting fields in the array, I just want to limit the elements in the array being published.
Thanks in advance for any responses!
EDIT
Here is an example document:
{
_id: 123456,
name: "Unit 1",
createdAt: (datetime object),
settings: *some stuff*,
packets: [
{
_id: 32412312,
temperature: 70,
createdAt: *datetime object from today*
},
{
_id: 32412312,
temperature: 70,
createdAt: *datetime from yesterday*
}
]
}
I want to get everything in this document except for the part of the array that was created more than 24 hours ago. I know I can accomplish this by moving the packets into their own collection and tying them together with keys as in a relational database but if what I am asking were possible, this would be simpler with less code.
You could do something like this in your publish method:
Meteor.publish("pubName", function() {
var collection = Collection.find().fetch(); //change this to return your data
_.each(collection, function(collectionItem) {
_.each(collectionItem.packets, function(packet, index) {
var deadline = Date.now() - 86400000 //should equal 24 hrs ago
if (packet.createdAt < deadline) {
collectionItem.packets.splice(index, 1);
}
}
}
return collection;
}
Though you might be better off storing the last 24 hours worth of packets as a separate array in your document. Would probably be less taxing on the server, not sure.
Also, code above is untested. Good luck.
you can use the $elemMatch projection
http://docs.mongodb.org/manual/reference/operator/projection/elemMatch/
So in your case, it would be
var today = new Date();
var yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
collection.find({}, //find anything or specifc
{
fields: {
'packets': {
$elemMatch: {$gt : {'createdAt' : yesterday /* or some new Date() */}}
}
}
});
However, $elemMatch only returns the FIRST element matching your condition. To return more than 1 element, you need to use the aggregation framework, which will be more efficient than _.each or forEach, particularly if you have a large array to loop through.
collection.rawCollection().aggregate([
{
$match: {}
},
{
$redact: {
$cond: {
if : {$or: [{$gt: ["$createdAt",yesterday]},"$packets"]},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}], function (error, result ){
});
You specify the $match in a way similar to find({}). Then all the documents that match your conditions get pipped into the $redact which is specified by the $cond.
$redact scans the document from top level to bottom. At the top level, you have _id, name, createdAt, settings, packets; hence {$or: [***,"$packets"]}
The presence of $packets in the $or allows the $redact to scan the second level which contain the _id, temperature and createdAt; hence {$gt: ["$createdAt",yesterday]}
This is async, you can use Meteor.wrapAsync to wrap around the function.
Hope this help

Update a subdocument with one single query or multiple (mongoose)?

This is a part of my document:
var actions = {
title: 'Change of windows'
energySavings:[
{
_id: 234324234,
mainCategory: 'Elektricitet',
subCategory: 'Belysning',
value: 1
},
{
_id: 5434534543,
mainCategory: 'Elektricitet',
subCategory: 'Utrustning',
value: 1
}
]
}
I want to be able to update energySavings, add items and remove items. This could be done in three separate queries. Or, I could all do it in just one
var givenAction = JSON.parse(req.body.action);
mongoose.model('Action').findOne({actionId: req.params.actionId}, function(err, action){
action.energySavings = givenAction.energySavings;
action.save(function(err, savedAction){
res.status(200).send(savedAction);
});
})
Way less code then three separate queries. However, if the client side programmer makes a mistake, for example empty the entire array and save it, we will have a problem. Is there a best practice to this, make in one query or several with more control?
You can also update the document with findOneAndUpdate of mongoose. For case the empty array added mistakenly, you need to check if the query is empty or not before updating or adding that.
var givenAction = JSON.parse(req.body.action);
mongoose.model('Action').findOneAndUpdate({
actionId: req.params.actionId
}, {
$set: {
energySavings: givenAction.energySavings
}
}, callback);
You can use $push to add as new items, and $pull to remove items instead of $set to updating it. I don't think it is not a good choice to update energySavings, add items and remove items in one query.

insert to specific index for mongo array

Mongo supports arrays of documents inside documents. For example, something like
{_id: 10, "coll": [1, 2, 3] }
Now, imagine I wanted to insert an arbitrary value at an arbitrary index
{_id: 10, "coll": [1, {name: 'new val'}, 2, 3] }
I know you can update values in place with $ and $set, but nothing for insertion. it kind of sucks to have to replace the entire array just for inserting at a specific index.
Starting with version 2.6 you finally can do this. You have to use $position operator. For your particular example:
db.students.update(
{ _id: 10},
{ $push: {
coll: {
$each: [ {name: 'new val'} ],
$position: 1
}
}}
)
The following will do the trick:
var insertPosition = 1;
var newItem = {name: 'new val'};
db.myCollection.find({_id: 10}).forEach(function(item)
{
item.coll = item.coll.slice(0, insertPosition).concat(newItem, item.coll.slice(insertPosition));
db.myCollection.save(item);
});
If the insertPosition is variable (i.e., you don't know exactly where you want to insert it, but you know you want to insert it after the item with name = "foo", just add a for() loop before the item.coll = assignment to find the insertPosition (and add 1 to it, since you want to insert it AFTER name = "foo".
Handy answer (not selected answer, but highest rated) from this similar post:
Can you have mongo $push prepend instead of append?
utilizes $set to insert 3 at the first position in an array, called "array". Sample from related answer by Sergey Nikitin:
db.test.update({"_id" : ObjectId("513ad0f8afdfe1e6736e49eb")},
{'$set': {'array.-1': 3}})
Regarding your comment:
Well.. with concurrent users this is going to be problematic with any database...
What I would do is the following:
Add a last modified timestamp to the document. Load the document, let the user modify it and use the timstamp as a filter when you update the document and also update the timestamp in one step. If it updates 0 documents you know it was modified in the meantime and you can ask the user to reload it.
Using the $position operator this can be done starting from version 2.5.3.
It must be used with the $each operator. From the documentation:
db.collection.update( <query>,
{ $push: {
<field>: {
$each: [ <value1>, <value2>, ... ],
$position: <num>
}
}
}
)