I have a collection with an array field ("array") that stores _ids which reference another collection. I am using mongoose's .populate and can populate a specific array element using the string dot notation, e.g.
.populate({path: "array.4"})
but I would now like to populate the last element of the array. In an ideal world, "array.-1" would work but it does not. I have tried using populate's match property with something like:
.populate({
path: "array",
match: {
$arrayElemAt: {
$subtract: [
{
$size: "array"
},
1
]
}
},
})
but this doesn't work either (and I don't think is how it's supposed to be used at all!)
Is there any way to achieve this?
Try this.
User.find({},{"arrayObject": {$slice: -1})
.populate('arrayObject')
Not sure if this is precisely what you're looking for but there is an easy way to do that for a specific document, which could help you do that for multiple. Let's imagine that your document's name is User and the array field that stores those ObjectIds is hobbies. So a sample doc may look like this:
{
"_id": ObjectId("5a934e000102030405000000"),
"hobbies": [
ObjectId("5a934e000102030405000001"),
ObjectId("5a934e000102030405000002")
],
"name": "James"
}
You could populate the last element of the hobbies array for a specific user doing this:
User.findById('5a934e000102030405000000')
.then(user => {
user.populate(`hobbies.${user.hobbies.length - 1}`, (err, u) => {
console.log(u);
});
})
If you have multiple docs you want populated, I would do it like this fully realizing that depending on how many docs you have, this make be time consuming:
User.find()
.then(users => {
const promises = users.map(user =>
user.populate(`hobbies.${user.hobbies.length - 1}`).execPopulate()
)
Promise.all(promises).then(pop => {
console.log(pop); //All populated with only last element of hobbies array
})
})
Related
Consider I have this document in my MongoDB collection, Workout:
{
_id: ObjectId("60383b491e2a11272c845749") <--- Workout ID
user: ObjectId("5fc7d6b9bbd9473a24d3ab3e") <--- User ID
exercises: [
{
_id: ObjectId("...") <--- Exercise ID
exerciseName: "Bench Press",
sets: [
{
_id: ObjectId("...") <--- Set ID
},
{
_id: ObjectId("...") <--- Set ID
}
]
}
]
}
The Workout object can include many exercise objects in the exercises array and each exercise object can have many set objects in the sets array. I am trying to implement a delete functionality for a certain set. I need to retrieve the workout that the set I want to delete is stored in. I have access to the user's ID (stored in a context), exercise ID and the set ID that I want to delete as parameters for the .findOne() function. However, I'm not sure whether I can traverse through the different levels of arrays and objects within the workout object. This is what I have tried:
const user = checkAuth(context) // Gets logged in user details (id, username)
const exerciseID, setID // Both of these are passed in already and are set to the appropriate values
const workoutLog = Workout.findOne({
user: user.id,
exercises: { _id: exerciseID }
});
This returns an empty array but I am expecting the whole Workout object that contains the set that I want to delete. I would like to omit the exerciseID from this function's parameters and just use the setID but I'm not sure how to traverse through the array of objects to access it's value. Is this possible or should I be going about this another way? Thanks.
When matching against an array, if you specify the query like this:
{ exercises: { _id: exerciseID } }
MongoDB tries to do an exact match on the document. So in this case, MongoDB would only match documents in the exercises array of the exact form { _id: ObjectId("...") }. Because documents in the exercises have other fields, this will never produce a match, even if the _ids are the same.
What you want to do instead is query a field of the documents in the array. The complete query document would then look like this:
{
user: user.id,
"exercises._id": exerciseID
}
You can perform both find and update in one step. Try this:
db.Workout.updateOne(
{
"user": ObjectId("5fc7d6b9bbd9473a24d3ab3e"),
},
{
$pull: {
"exercises.$[exercise].sets": {
"_id": ObjectId("6039709fe0c7d52970d3fa30") // <--- Set ID
}
}
},
{
arrayFilters: [
{
"exercise._id" : ObjectId("6039709fe0c7d52970d3fa2e") // <--- Exercise ID
}
]
}
);
This is my problem : I have a document ( let's call it root) containing an array of another documents (stick), that contain another array of another documents (leaf).
Or simply said : root{ stickChain[leaveschain1[ leaf1, leaf2],leaveschain2[ leaf1, leaf2] ]}
I have access to root _id, stick _id, or whatever it is needed to identify each document.
Basically, the best result I've come so far is, when creating my leaves documents, is to store then at the same level tha sticks, or in another word I've come to create an array of leaves in root.
I'm working in javascript and using mongoose
This is the line I've used:
db.collection('root').findOneAndUpdate({ _id: root.id, "stick._id":expTemp._id },{stickChain:{$push: {"leavechain.$": leaf}}})
And I this gives me this result : root{ leavesChain[leaf1,leaf2], stickchain[1,2,3] }
I've come across something new to me (since Mongodb 3.6 there is a new way of handling array of arrays), here is what I've tried :
try{ db.collection('root').findOneAndUpdate(
{ _id: root.id, "stick._id":expTemp._id },
{$push: {"stickChain.$[xpc].leavesChain.$[qac]": leaf}},
{ arrayFilters: [ { "xpc._id": user._id } , { "qac._id": expTemp._id } ]})}
UPDATE
try{ db.collection('root').findAndModify(
{$push: {"root.$[cv].stickChain.$[xpc].leavesChain.$[qac]": leaf}},
{ arrayFilters: [ {"cv._id":user.cv} ,{ "xpc._id": user._id } , { "qac._id": expTemp._id } ],
upsert:true})
.catch(error => console.error(error))
}catch{}
And this gives me a new kind of error : MongoError: Either an update or remove=true must be specified
The thing is that I'm not familiar with how to do it, while I know what I want to do: I want to insert a "leaf" into a specific array in a document fetched in MongoDB. Maybe not the best practice, so any hint are welcome :)
I've splitted my dument like this :
root[stickChain _id]
stick[leavesChain[leaf]]
Thanks to Andrey Popov for his explications
Please help me solve this, I would like to update the fields using dot notation, using set() but each time I run with the below implementation. I have the fields added to firestore as e.g studentInfo.0.course.0.courseId instead of updating the already existing ones.
Json sample as it sits in firestore
"schoolId": "school123",
"studentInfo": [
{
"studentId": "studentI23",
"regDate": "2020-04-18",
"course": [
{
"courseId": "cs123",
"regDate": "2020-05-28",
"status": "COMPLETED"
}
]
}
],
"registered":"yes"
}
Code logic
const query = firestore.collection('users').where('registered', '==', 'yes')
const students = await query.get()
students.forEach(student => {
firestore.doc(student.ref.path).set({
'studentInfo.0.studentId': '345','studentInfo.0.course.0.courseId': '555'
}, { merge: true })
})
On the docs https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects I can only find updating nested objects but not nested array objects.
It is indeed not possible to update a single element in an array using dot notation, or otherwise. To update an array you'll need to:
Read the document
Get the current value of the array from it
Determine the new array contents
Write the entire updated array back to the database.
The only alternative array operations are array-union and array-remove, which add and remove unique elements to/from the array - essentially treating it as a mathematical set. But since you are looking to update an existing element, these operations are of no use here.
Also see:
Firestore Update single item in an array field
Firestore update specific element in array
How to update an "array of objects" with Firestore?
There is no direct way to update the as stated in the article. You can either run a transaction to get the latest array value and then updating the array with the final array value. That would be as below:
await firestore.runTransaction((transaction: Transaction) => {
const students: Array<Students> = firestore
.collection("users")
.where("registered", "==", "yes");
students.forEach((student) => {
const firebaseDoc = firestore.doc(student.ref.path);
transaction.set(
firebaseDoc,
{
"studentInfo.0.studentId": "345",
"studentInfo.0.course.0.courseId": "555",
},
{ merge: true }
);
});
});
Inside transaction I am getting the array first and then updating each values as per my need. This will make the whole operation atomic so the issues mentioned in the article will not come.
Alternatively, you can also model your firestore database as below
"schoolId": "school123",
"studentInfo": {
"studentI23": {
"studentId": "studentI23",
"regDate": "2020-04-18",
"course": [
{
"courseId": "cs123",
"regDate": "2020-05-28",
"status": "COMPLETED"
}
]
}
},
"registered":"yes"
}
Above I have changed the array to map, since in map you can update the each field based on dot notation fields(doc), hence. you can achieve your end result. This solution will avoid any transaction query and will be faster
When I have one item left in an array and I remove it, the field becomes empty. Is there any option to check if there is one element left and then update the key with undefined instead of removing?
Here is what I have:
User.aggregate([
{$match: {email: {$in: [email, friendEmail]}}},
{
$project: {
friendSentRequests: {$size: {"$ifNull": ["$friendSentRequests", []]}},
friendReceivedRequests: {$size: {"$ifNull": ["$friendReceivedRequests", []]}},
}
}],
(err, result) => { ... }
);
With method written above I indeed get the array size, but batch throws errors that array is type missing if one doc doesn't have the key that second doc has. Unless I'm not supposed to make any updates in (err, result) => {..} function and there is another recommended way to update the object.
I don't want to set multi because I want to update via batch two different keys from different docs and I can't get the array length of each of these fields. Unless I'm misunderstanding how multi works.
if you want to confirm the number of elements in an array , e.g here is you want to check that the array has one element then:
{
$match:{
"myArray" :{ $size : 1 }
}
}
And to check if array does not exist:
{
$match:{
"myArray" :{ $exists: false}
}
}
I'm building a Node.js application to keep an inventory of books and am using MongoDB and mongoose.
When I first modeled my Schema, there was a genre field of type String which held a single value; in other words, a book could only be assigned one genre.
After adding some "books" into my database, I decided to make a slight change to the Schema design and updated the genre field to genres so that I could instead have an Array of strings. Now every new document I add into the database has a genres field of type Array, but now I'm wondering how to update the documents in the database which reflect the design of the earlier model.
I've started to write a script that:
Iterates through documents in the database where the field genre (singular) exists,
Updates the field name to genres AND sets its value to an array containing whatever value the previous genre property was equal to.
So for instance, (for simplicity's sake I'll use an object with a single property of genre), from this:
{genre: "Philosophy"}
I'm trying to get to this
{genres: ["Philosophy"]}
How can I achieve this?
This is the code I have so far
db.books.updateMany({genre: {$exists: true}},{<missing this part>}
You can use aggregation framework to do that:
db.books.aggregate([
{ $match: { genre: { $exists: true } } },
{
$group: {
_id: "$_id",
genres: { $push: "$genre" },
fst: { $first: "$$ROOT" }
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: ["$fst", "$$ROOT" ] } }
},
{ $project: { fst: 0, genre: 0 } },
{ $out: "books" }
])
$group is a way of transforming property to an array (each group will contain only one element since we're grouping by unique _id). Using $replaceRoot with $mergeObjects you can merge all your properties from original object with your new array. Then you need to remove unnecessary fields using $project. Last step which is $out will redirect the output of this aggregation into specified collection, causing updates in this case.
You want to iterate over every element in your db:
db.books.find({genre: {$exists: true}}).forEach(doc => {
Then you want to update that element:
if(Array.isArray(doc.genre)) return;
db.books.updateOne(doc, { $set: { genre:[doc.genre]} });