Mongodb/Mongoose: Filling Array of ObjectIds (references) - javascript

I have a mongodb running (MEAN environment) with two collections (users and books). One of those collections (myusers) contains an array of Objectids (references to documents of books collection) as such:
var UserSchema = new Schema({
[...],
externalids: [{type: Schema.Types.ObjectId, ref: 'Books', required: false}]
}, {collection: 'myusers'});
At runtime, I'd like to constantly fill that array (externalids) with ids of new book documents. This is how I do it:
var newBook = new Book({ ... });
newBook.save(function (err){
if (err){
//whatever
}else{
User.update({ _id: req.user._id }, { $set: { externalids: newBook._id }}, function(err){
//whatever
});
}
});
Unfortunately, I can't use something like:
externalids: externalids.push(newBook._id)
I even tried:
User.update({ _id: req.user._id }, { $push: { externalids: newBook._id }}
But it wouldn't help either.
Thus, the array always only contains one value (the latest) and won't be filled up. Is there a quick way to append more values? Would be nice if there was a quicker way than reading the array content first, storing it to a local temporary array, append the new value and write the entire array back...
Cheers
Igor

Try $addToSet instead of $push to avoid duplicate id.
User.update({ _id: req.user._id }, { $addToSet : { externalids: newBook._id }}
What can be your issue is that you previously use $set to update an user. This will initially create externalids as string, not array, hence you cannot use $push/$addToSet on externalids afterwards. To correct that, try to update your user externalids as an array first:
User.update({ _id: req.user._id }, { $set : { externalids: [] }}

Related

Mongoose - Deleting documents is unresponsive

I'm trying to use Mongoose (MongoDB JS library) to create a basic database, but I can't figure out how to delete the documents / items, I'm not sure what the technical term for them is.
Everything seems to work fine, when I use Item.findById(result[i].id), it returns a valid id of the item, but when I use Item.findByIdAndDelete(result[i].id), the function doesn't seem to start at all.
This is a snippet the code that I have: (Sorry in advance for bad indentation)
const testSchema = new schema({
item: {
type: String,
required: true
},
detail: {
type: String,
required: true
},
quantity: {
type: String,
required: true
}
})
const Item = mongoose.model("testitems", testSchema)
Item.find()
.then((result) => {
for (i in result) {
Item.findByIdAndDelete(result[i].id), function(err, result) {
if (err) {
console.log(err)
}
else {
console.log("Deleted " + result)
}
}
}
mongoose.connection.close()
})
.catch((err) => {
console.log(err)
})
I'm not sure what I'm doing wrong, and I haven't been able to find anything on the internet.
Any help is appreciated, thanks.
_id is a special field on MongoDB documents that by default is the type ObjectId. Mongoose creates this field for you automatically. So a sample document in your testitems collection might look like:
{
_id: ObjectId("..."),
item: "xxx",
detail: "yyy",
quantity: "zzz"
}
However, you retrieve this value with id. The reason you get a value back even though the field is called _id is because Mongoose creates a virtual getter for id:
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it by passing this option at schema construction time.
The key takeaway is that when you get this value with id it is a string, not an ObjectId. Because the types don't match, MongoDB will not delete anything.
To make sure the values and types match, you should use result[i]._id.

Add to an array - sub-document without duplicate field values

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 } },
)

What is the best way to keep track of changes of a document's property in MongoDB?

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 });

mongoose extract nested arrays from multiple objects into one array

const userSchema = new Schema(
{
_id: Schema.Types.ObjectId,
name: String,
posts: [{ type: Schema.Types.ObjectId, ref: "Post" }],
following: [{ type: Schema.Types.ObjectId, ref: "User" }]
}
};
I want to extract all the posts from all the Users in the 'following' array, put them into one single array, sort them and then display the first 20. I was wondering if that is possible within the cursor or if I have to load it into memory.
function createFeed(user) {
User.findOne({ name: user })
.populate({
path: "following",
populate: {
path: "posts"
}
})
//put all the posts into one array
.sort(...) //sort by time created
.limit(...) //only get the newest n posts
.exec((err, result) => {
if (err) console.log("error", err);
console.log("result", //sorted tweets array);
});
};
(I don't want to filter all the posts in my 'Posts' collection to check if they are made by the user since that would be a lot more expensive)
You can use distinct query in mongoDB
db.User.distinct('following',{})
If you are trying to filter your populate with a condition, then you should be doing this:
User.findOne({ name: user })
.populate({
path: 'posts',
match: { user: 'XXX' }
})
Even more better would be to query the posts with the user filter condition and then populate user details.

Trying to understand the use of the populate method

I see that one way we use populate is to put one document from another collection into a "parent" collection. I was just going through this question and I was hoping someone could explain the answer to me better. And show me a practical use. Here is an example from the answer.
var PersonSchema = new mongoose.Schema({
t: String
}, {collection: 'persons'});
var User = mongoose.model('User', PersonSchema.extend({
_id: String,
name: String
}));
var ParentSchema = new mongoose.Schema({
s: String
}, {collection: 'parent'});
var Like = mongoose.model('Like', ParentSchema.extend({
_id: String,
user_id: {
type: String,
ref: 'User'
}
}));
Insert Data into DB,
var user = new User({
t: 't1',
_id: '1234567',
name: 'test'
});
var like = new Like({
s: 's1',
_id: '23456789',
});
user.save(function(err, u){
if(err)
console.log(err);
else {
like.user_id = u._id;
console.log(like);
like.save(function(err) {
if (err)
console.log(err);
else
console.log('save like and user....');
});
}
});
Query by
Like.findOne({}).populate('user_id').exec(function(err, doc) {
if (err)
console.log(err);
else
console.log(doc);
});
And the result is
{ _id: '23456789',
__t: 'Like',
user_id: { _id: '1234567', __t: 'User', t: 't1', name: 'test', __v: 0 },
s: 's1',
__v: 0 }
QUESTION
where does __t: 'User' come from?
I was thinking that using populate() or ref that would separate the collections but it looks like at the end the like collection has the users document in it. I think I wanted to use populate so I could make a document smaller.
3.Also if someone really wanted to help explain this to me I have an example that I have been trying to do and I don't know if I should use populate but if I should it would be great if you show me how. Here is the example.
You have
doctors
patients
information about the practice
There could be like a 1000 doctors and lots of patients for each doctor. and the information will be about their practice(like how many employees they have). so I feel that there should be a separation of concern.(one reason is to prevent a single document for a patient from getting to big). So If we're going with the populate method If you could explain how to set it up for this case. I guess I could have a doctor as a parent and a child refs for patients and another child refs for information about practice. so maybe there should be an array of objectId for the patients and an array for Other information
Q1: where does __t: 'User' come from?
Refer to this link.
mongoose now includes schema inheritance and discriminatorKey functionality that breaks mongoose-schema-extend. mongoose now sets the discriminatorKey schema option to __t by default
Q2: I was thinking that using populate() or ref that would separate the collections but it looks like at the end the like collection has the users document in it. I think I wanted to use populate so I could make a document smaller.
It seems you misunderstand the meaning of Population. There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where population comes in. Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s). So populate is not used to make document smaller.
Q3: Doctor, Patient, Practice
Schema could be as following:
var DoctorSchema = new Schema ({
name: String,
// ... other field
});
var PatientSchema = new Schema ({
name: String,
doctor: {type: Schema.ObjectId,
ref: 'Doctor'}
});
var PracticeSchema = new Schema ({
ff: String,
patientId: {type: Schema.ObjectId,
ref: 'Patient'},
doctorId: {type: Schema.ObjectId,
ref: 'Doctor'}
});
As for schema, it is hard to determine which schema is better or not, (with populate or without it). The first thing we should consider is to meet our query requirement, to make the query easy. The design of mongoDB to make the query more efficiently. So our schema should meet it.

Categories