I would like to preface this with saying that english is not my mother tongue, if any of my explanations are vague or don't make sense, please let me know and I will attempt to make them clearer.
I have a document containing some nested data. Currently product and customer are arrays, I would prefer to have them as straight up ObjectIDs.
{
"_id" : ObjectId("5bab713622c97440f287f2bf"),
"created_at" : ISODate("2018-09-26T13:44:54.431Z"),
"prod_line" : ObjectId("5b878e4c22c9745f1090de66"),
"entries" : [
{
"order_number" : "123",
"product" : [
ObjectId("5ba8a0e822c974290b2ea18d")
],
"customer" : [
ObjectId("5b86a20922c9745f1a6408d4")
],
"quantity" : "14"
},
{
"order_number" : "456",
"product" : [
ObjectId("5b878ed322c9745f1090de6c")
],
"customer" : [
ObjectId("5b86a20922c9745f1a6408d5")
],
"quantity" : "12"
}
]
}
I tried using the following query to update it, however that proved unsuccessful as Mongo didn't behave quite as I had expected.
db.Document.find().forEach(function(doc){
doc.entries.forEach(function(entry){
var entry_id = entry.product[0]
db.Document.update({_id: doc._id}, {$set:{'product': entry_id}});
print(entry_id)
})
})
With this query it sets product in the root of the object, not quite what I had hoped for. What I was hoping to do was to iterate through entries and change each individual product and customer to be only their ObjectId and not an array. Is it possible to do this via the mongo shell or do I have to look for another way to accomplish this? Thanks!
In order to accomplish your specified behavior, you just need to modify your query structure a bit. Take a look here for the specific MongoDB documentation on how to accomplish this. I will also propose an update to your code below:
db.Document.find().forEach(function(doc) {
doc.entries.forEach(function(entry, index) {
var productElementKey = 'entries.' + index + '.product';
var productSetObject = {};
productSetObject[productElementKey] = entry.product[0];
db.Document.update({_id: doc._id}, {$set: productSetObject});
print(entry_id)
})
})
The problem that you were having is that you were not updating the specific element within the entries array, but rather adding a new key to the top-level of the document named product. Generally, in order to set the value of an inner document within an array, you need to specify the array key first (entries in this case) and the inner document key second (product in this case). Since you are trying to set specific elements within the entries array, you need to also specify the index in your query object, I have specified above.
In order to update the customer key in the inner documents, simply switch out the product for customer in my above code.
You're trying to add a property 'product' directly into your document with this line
db.Document.update({_id: doc._id}, {$set:{'product': entry_id}});
Try to modify all your entries first, then update your document with this new array of entries.
db.Document.find().forEach(function(doc){
let updatedEntries = [];
doc.entries.forEach(function(entry){
let newEntry = {};
newEntry["order_number"] = entry.order_number;
newEntry["product"] = entry.product[0];
newEntry["customer"] = entry.customer[0];
newEntry["quantity"] = entry.quantity;
updatedEntries.push(newEntry);
})
db.Document.update({_id: doc._id}, {$set:{'entries': updatedEntries}});
})
You'll need to enumerate all the documents and then update the documents one and a time with the value store in the first item of the array for product and customer from each entry:
db.documents.find().snapshot().forEach(function (elem) {
elem.entries.forEach(function(entry){
db.documents.update({
_id: elem._id,
"entries.order_number": entry.order_number
}, {
$set: {
"entries.$.product" : entry.product[0],
"entries.$.customer" : entry.customer[0]
}
}
);
});
});
Instead of doing 2 updates each time you could possibly use the filtered positional operator to do all updates to all arrays items within one update query.
Related
I'm new to MongoDB and have to work on a legacy project that I didn't create... and I'm struggling!
I need to reset some documents in one of my collections, based on a particular field value. I have had some success with this so far, but some of the data I need to update is within nested arrays in that document, and I can't work that part out.
Below is an example of one document in the collection:
{
"_id" : ObjectId("1234567890"),
"currentStatus" : "approved",
"itemsInstalled" : [
{
"installDate" : ISODate("2017-04-18T00:00:00.000Z"),
"_id" : ObjectId("1234567890"),
"status" : "approved"
},
{
"installDate" : ISODate("2017-04-18T00:00:00.000Z"),
"_id" : ObjectId("0987654321"),
"status" : "approved"
}
],
"__v" : 5005,
"approvalDate" : ISODate("2017-12-04T10:40:01.580Z"),
"approvedBy" : "automatic"
}
I need to update every document in my collection where the approvedBy field is set to automatic, and leave the others untouched.
For the documents I am updating, I need to remove the approvalDate and approvedBy fields completely, change the currentStatus field to action and change every item in the itemsInstalled array to pending. Everything else can stay as it is.
This is something I would persist with solving myself if I had more time.
Unfortunately, to my knowledge, you can't update multiple array elements. Your best bet is probably to use forEach. You can accomplish what you want with something like this:
db.your_collection.find({
approvedBy: "automatic"
}).forEach(function(doc) {
for(var i = 0; i < doc.itemsInstalled.length; i++) {
doc.itemsInstalled[i].status = "pending";
}
doc.currentStatus = "action";
delete doc.approvedBy;
delete doc.approvalDate;
db.your_collection.update({_id: doc._id}, doc);
});
Using forEach, you can update all of the array elements at once. The downside is that you will be performing multiple update queries, so you should be careful about doing this on particularly large collections or as part of your application logic. Ideally this should be a one-time use scenario.
I have a structure which looks like below:
course: { // course
id,
coursename,
sections: { // has an array of sections
section: {
id,
sectionname
cards: { // each section has an array of cards
card: {
id
cardname
}
}
}
}
}
Now, I am given the card ID. Suppose cardID is 301. I don't have details about the course or the section in which the card is stored. I only have the cardID. I want to update the cardname from "Test 1" to "Test 2".
Please provide some information about how to update the name of card given only the cardId. Also please provide information about how I can access a single card using only cardId.
If i run the following query:
db.course.find( {"sections.cards.id" : ObjectId("5859517447c4286d0f0639ee")},
{"sections.cards.$":1,_id:0})
The whole section object is returned instead of returning a single card. Please provide some infromation about how to access a single card using only cardId.
An example of document that i have:
{
"_id" : ObjectId("58595041cfdc46100f92a985"),
"modelType" : "Course",
"name" : "c1",
"sections" : [
{
"id" : ObjectId("5859504dcfdc46100f92a986"),
"name" : "s1",
"cards" : [
{
"id" : ObjectId("58595057cfdc46100f92a987"),
"name" : "card1",
},
{
"id" : ObjectId("5859506ccfdc46100f92a988"),
"name" : "card2"
}
]
}
]
}
Please provide information about how to update a card using only card Id.
Suppose I want to change the card's name. How do I achieve it?
I hope this answer is enough for your needs:
db.collection.aggregate([
{$unwind: "$sections"}, // unwind the array
{$unwind: "$sections.cards"}, // unwind the array
{$match: {"sections.cards.id": ObjectId("5859506ccfdc46100f92a988")}}, // filter
{$project: {_id: 0, id: "$sections.cards.id", name: "$sections.cards.name"}} // cut and clear any property that you don't need
])
which result in(I tested it using your sample):
{
"name" : "card2",
"id" : ObjectId("5859506ccfdc46100f92a988")
}
Explanation:
First off, you need aggregate(find any library like mongojs or mongoose and how to do this within their respective manual),
then unwind sections and cards inside it.
After both steps, you need to filter documents resulted from unwind operation according to your requirement. In this case: which card match your ID
Clear any properties that you don't need
Return the result
To Update properties inside a document:
It's impossible to update an element inside of an array without knowledge of its index, so first, you must find out its index/position inside. The simplest way I could think of is by using for (since yours is double tier-ed array, I think mapReduce method will be much more complicated):
// first, find the document, where xxx is the target card ID
db.collection.find({'sections.cards.id': ObjectId("xxx")}, function(err, reply){
// iterates through the documents
for(var z = 0; z < reply.length; z++){
// iterates through the `sections`
for(var y = 0; y < reply[z].sections.length; y++){
// iterates through the `cards`
for(var x = 0; x < reply[z].sections[y].cards.length; x++){
if(reply[z].sections[y].cards[x].id.toString() === "xxx")
{
// do what you want to do the card here
reply[z].sections[y].cards[x].name = "updated name";
}
}
}
// save the updated doc one-by-one,
db.collection.update({_id: reply._id}, reply);
}
});
The best solution
The best solution is: Don't have this kind of document in the first place. schema-free database approach DOESN'T mean that you can put everything inside a document. You had to consider the functionality and accessibility of data inside next time you design the document schema.
You can rationalize the documents according to your needs. In this case, you should split the course document, since it clear that card property should be an independent collection from course. If you worry that you'll need to join both collection, worry not. Since 3.2 mongodb introduced $lookup for this kind of application.
Not only it'll make your life easier, it'll make mongo queries run faster too.
How can I return a set of documents, each not containing a specific item in an inner array?
My data scheme is:
Posts:
{
"_id" : ObjectId("57f91ec96241783dac1e16fe"),
"votedBy" : [
{
"userId" : "101",
"vote": 1
},
{
"userId" : "202",
"vote": 2
}
],
"__v" : NumberInt(0)
}
I want to return a set of posts, non of which contain a given userId in any of the votedBy array items.
The official documentation implies that this is possible:
MongoDB documentation: Field with no specific array index
Though it returns an empty set (for the more simple case of finding a document with a specific array item).
It seems like I have to know the index for a correct set of results, like:
votedBy.0.userId.
This Question is the closest I found, with this solution (Applied on my scheme):
db.collection.find({"votedBy": { $not: {$elemMatch: {userId: 101 } } } })
It works fine if the only inner document in the array matches the one I wish not to return, but in the example case I specified above, the document returns, because it finds the userId=202 inner document.
Just to clarify: I want to return all the documents, that NONE of their votedBy array items have the given userId.
I also tried a simpler array, containing only the userId's as an array of Strings, but still, each of them receives an Id and the search process is just the same.
Another solution I tried is using a different collection for uservotes, and applying a lookup to perform a SQL-similar join, but it seems like there is an easier way.
I am using mongoose (node.js).
User $ne on the embedded userId:
db.collection.find({'votedBy.userId': {$ne: '101'}})
It will filter all the documents with at least one element of userId = "101"
I have a collection where the _id is of the form [message_code]-[language_code] and another where the _id is just [message_code]. What I'd like to do is find all documents from the first collection where the message_code portion of the _id does not appear in the second collection.
Example:
> db.colA.find({})
{ "_id" : "TRM1-EN" }
{ "_id" : "TRM1-ES" }
{ "_id" : "TRM2-EN" }
{ "_id" : "TRM2-ES" }
> db.colB.find({})
{ "_id" : "TRM1" }
I want a query that will return TRM2-EN and TRM-ES from colA. Of course in my live data, there are thousands of records in each collection.
According to this question which is trying to do something similar, we have to save the results from a query against colB and use it in an $in condition in a query against colA. In my case, I need to strip the -[language_code] portion before doing this comparison, but I can't find a way to do so.
If all else fails, I'll just create a new field in colA that contains only the message code, but is there a better way do it?
Edit:
Based on Michael's answer, I was able to come up with this solution:
var arr = db.colB.distinct("_id")
var regexs = arr.map(function(elm){
return new RegExp(elm);
})
var result = db.colA.find({_id : {$nin : regexs}}, {_id : true})
Edit:
Upon closer inspection, the above method doesn't work after all. In the end, I just had to add the new field.
Disclaimer: This is a little hack it may not end well.
Get distinct _id using collection.distinct method.
Build a regular expression array using Array.prototype.map()
var arr = db.colB.distinct('_id');
arr.map(function(elm, inx, tab) {
tab[inx] = new RegExp(elm);
});
db.colA.find({ '_id': { '$nin': arr }})
I'd add a new field to colA since you can index it and if you have hundreds of thousands of documents in each collection splitting the strings will be painfully slow.
But if you don't want to do that you could make use of the aggregation framework's $substr operator to extract the [message-code] then do a $match on the result.
I have recently changed one of my fields from object to array of objects.
In my production I have only 14 documents with this field, so I decided to change those fields.
Is there any best practices to do that?
As it is in my production I need to do it in a best way possible?
I got the document Id's of those collections.like ['xxx','yyy','zzz',...........]
my doc structure is like
_id:"xxx",option1:{"op1":"value1","op2":"value2"},option2:"some value"
and I want to change it like(converting object to array of objects)
_id:"xxx",option1:[{"op1":"value1","op2":"value2"},
{"op1":"value1","op2":"value2"}
],option2:"some value"
Can I use upsert? If so How to do it?
Since you need to create the new value of the field based on the old value, you should retrieve each document with a query like
db.collection.find({ "_id" : { "in" : [<array of _id's>] } })
then iterate over the results and $set the value of the field to its new value:
db.collection.find({ "_id" : { "in" : [<array of _id's>] } }).forEach(function(doc) {
oldVal = doc.option1
newVal = compute_newVal_from_oldVal(oldVal)
db.collection.update({ "_id" : doc._id }, { "$set" : { "option" : newVal } })
})
The document structure is rather schematic, so I omitted putting in actual code to create newVal from oldVal.
Since it is an embedded document type you could use push query
db.collectionname.update({_id:"xxx"},{$push:{option1:{"op1":"value1","op2":"value2"}}})
This will create document inside embedded document.Hope it helps