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
}
]
}
);
Related
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
})
})
I am trying to add an object to an array in MongoDB. I don't want it to be duplicated.
I am trying to update the user read array by using $addToset in findOneAndUpdate. However, it is inserting duplicate because of timestamp; the timestamp is an important property. I can't negate it. Can I insert based on key like userId? Please let me know.
{
_id: 'ddeecd8b-79b5-437d-9026-d0663b53ad8d',
message: 'hello world notification',
deliverToUsersList: [ '123-xxx-xx', '124-xxx-xx']
userRead: [
{
isOpened: true,
userId: '123-xxx-xx'
updatedOn: new Date(Date.now()).toISOString()
},
{
isOpened: true,
userId: '124-xxx-xx'
updatedOn: new Date(Date.now()).toISOString()
}
]
}
Add an index to the field userId and enable 'Avoid duplicates' in index settings.
I use Robo3T client to do that.
To add new objects without duplicate information into the userRead array, you have check for the duplicate information in the update method's query filter. For example, the following code will not allow adding new object with duplicate userId field value.
new_userId = "999-xxx-xx"
new_doc = { userId: new_userId, isOpened: true, updatedOn: ISODate() }
db.test_coll.findOneAndUpdate(
{ _id: 'ddeecd8b-79b5-437d-9026-d0663b53ad8d', "userRead.userId": { $ne: new_userId } },
{ $push: { "userRead" : new_doc } },
)
I would like to know how to keep track of the values of a document in MongoDB.
It's a MongoDB Database with a Node and Express backend.
Say I have a document, which is part of the Patients collection.
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Burn fat"
}
Then I edit the "objective" property, so the document results like this:
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Gain muscle"
}
What's the best/most efficient way to keep track of that change? In other words, I would like to know that the "objective" property had the value "Burn fat" in the past, and access it in the future.
Thanks a lot!
Maintaining/tracking history in the same document is not all recommended. As the document size will keep on increasing leading to
probably if there are too many updates, 16mb document size limit
Performance degrades
Instead, you should maintain a separate collection for history. You might have use hibernates' Javers or envers for auditing for your relational databases. if not you can check how they work. A separate table (xyz_AUD) is maintained for each table (xyz). For each row (with primary key abc) in xyz table, there exist multiple rows in xyz_AUD table, where each row is version of that row.
Moreover, Javers also support MongoDB auditing. If you are using java you can directly use it. No need to write your own logic.
Refer - https://nullbeans.com/auditing-using-spring-boot-mongodb-and-javers/
One more thing, Javers Envers Hibernate are java libraries. But I'm sure for other programming languages also, similar libraries will be present.
There is a mongoose plugin as well -
https://www.npmjs.com/package/mongoose-audit (quite oudated 4 years)
https://github.com/nassor/mongoose-history#readme (better)
Maybe you can change the type of "objective" to array and track the changes in it. the last one of the array is the latest value.
Maintain it as a sub-document like below
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": {
obj1: "Gain muscle",
obj2: "Burn fat"
}
}
You can also maintain it as an array field but remember, mongodb doesn't allow you to maintain uniqueness in an array field and if you plan to index the "objective" field, you'll have to create a multi key index
I think the simplest solution would be to use and update an array:
const patientSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
objective: { type: String, required: true }
notes: [{
date: { type: Date, default: Date.now() },
note: { type: String, required: true }
}],
});
Then when you want to update the objective...
const updatePatientObjective = async (req, res) => {
try {
// check if _id and new objective exist in req.body
const { _id, objective, date } = req.body;
if (!_id || !objective) throw "Unable to update patient's objective.";
// make sure provided _id is valid
const existingPatient = await Patient.findOne({ _id });
if (!existingPatient) throw "Unable to locate that patient.";
// pull out objective as previousObjective
const { objective: previousObjective } = existingPatient;
// update patient's objective while pushing
// the previous objective into the notes sub document
await existingPatient.updateOne({
// update current objective
$set { objective },
// push an object with a date and note (previouseObjective)
// into a notes array
$push: {
notes: {
date,
note: previousObjective
},
},
}),
);
// send back response
res
.status(201)
.json({ message: "Successfully updated your objective!" });
} catch (err) {
return res.status(400).json({ err: err.toString() });
}
};
Document will look like:
firstName: "John",
lastName: "Smith",
objective: "Lose body fat.",
notes: [
{
date: 2019-07-19T17:45:43-07:00,
note: "Gain muscle".
},
{
date: 2019-08-09T12:00:38-07:00,
note: "Work on cardio."
}
{
date: 2019-08-29T19:00:38-07:00,
note: "Become a fullstack web developer."
}
...etc
]
Alternatively, if you're worried about document size, then create a separate schema for patient history and reference the user's id (or just store the patient's _id as a string instead of referencing an ObjectId, whichever you prefer):
const patientHistorySchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Patient", required: true },
objective: { type: String, required: true }
});
Then create a new patient history document when the objective is updated...
PatientHistory.create({ _id, objective: previousObjective });
And if you need to access to the patient history documents...
PatientHistory.find({ _id });
Below is an image of what my user MongoDB document looks like. I have a skills array which contains objects with the follwing structure.
{
name: String,
points: Number,
skill: Schema.Types.ObjectId
}
Here's a screenshot of an actual user document, you can see the skill with the name html
Now I want to create a search query that would match the name property of one these objects in the skills array. e.g if my input is htm it would match with a user that has a skill with the name html. I tried it the way below, but it doesn't seem to be working. Can someone suggest me how to successfully do this?
const createSkillsQuery = (user, input) => User.find({
$and: [
{ skills: { name: { $regex: input, $options: 'i' } } },
{ _workspace: user._workspace }
]
}).select('profile_pic full_name email created_date');
You need to use the dot notation here.
"skills.name": { $regex: input, $options: 'i' }
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]} });