How to find documents by some conditions for its linked documents - node.js

I am a newby in MongoDB and I have a problem when querying a linked documents of the some documents collection.
Here is my database scheme:
var tagScheme = Schema({
name: { type: String, required: true }
});
tagScheme.index({ name: 1 }, { unique: true });
var linkScheme = Schema({
name: { type: String },
tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }]
});
linkScheme.index({ name: 1 }, { unique: true });
I need to get a count of the appropriate links for the specified tag. I try to execute the following query:
dbschemes.Link.find({ 'tags.name': specifiedTagName }, function (err, links) {
return res.send(500, err);
alert(links.length);
});
This query works not properly: it always returns an empty links list. Could someone exlain me what the problem is?

As JohnnyHK commented, the type of query you want to do is a relational type query and document database such as mongodb simply do not support them. Fix your schema to put that tag data directly in the link schema (nesting or "denormalizing" from a relational standpoint, which is OK in this case), then you can query it:
var LinkSchema = new Schema({
name: String,
tags: [String]
});
With that Schema, your query will work as you expect.
To address the comments below. This is a document database. It's not relational. There are trade-offs. Your data is de-normalized and it gives you some scalability and performance and you trade off flexibility of queries and data consistency to get them. If you wanted to rename a tag, a relatively rare occurrence, you'd have to do a whopping 2 database commands (a $push of the new name then a $pull of the old name) as opposed to relation where a single update command would do it.

Related

Why is an array in my mongoose model preventing me from querying

I have the following mongoose model
CartSchema = new Schema({
company: String,
items: [{
_id: Schema.ObjectId,
price: Number,
gears: [String]
}],
});
I access it via this simpe query
const response = await Cart.findOne( { "items": { _id: "5e4d7a5bcff77131f46d8aa9" } });
And this is my data in the mongo database
So from this information we can see that the only information that I have in my database that corresponds to the model is the items[0]._id which should be found with the query above. The odd thing is it returns null as long as this line gears: [String], is in my model. It is not required (I also tried setting it manually to required : false but I can't seem to get my data if this line is in my model. If I remove the line from my model, I can get the data just fine.
Am I missing something obvious here that would prevent me from getting my data because of the gears: [String] line in my model?
by this way, { "items": { _id: "5e4d7a5bcff77131f46d8aa9" } }, you're searching for an exact match, the items should be an object contains only the specified _id
instead, you should use the dot notation to filter by the _id in the items array
const response = await Cart.findOne( { "items._id": "5e4d7a5bcff77131f46d8aa9" });
hope it helps

Mongoose filter nested array in pre find query

I have the following issue. I have some comments that are soft-deletable. So they have a flag is_deleted and it is set to true when a record is deleted.
My comments aren't an independent model, but they are a nested array in another model (simplified model):
let CommentSchema = new Schema({
text: {
type: String,
required: true
},
modified_at: {
type: Date,
default: null
},
created_at: {
type: Date,
default: Date.now
},
is_deleted: {
type: Boolean,
default: false
},
});
let BookSchema = new Schema({
...
comments: [CommentSchema],
...
});
Now when I get all my Books with Books.find({}, (err, books) => {}) I wanted to filter out the deleted comments in:
BookSchema.pre('find', function() {
this.aggregate(
{},
{ $unwind: '$comments'},
{ $match: {'comments.is_deleted': false}})
});
But it does not work. Any idea how to write the query, so that it only return the non-deleted nested comments without creating an independent Comment collection?
EDIT: I didn't mention it's an endpoint where I access only one book object. The resource url is like this: /books/{bookId}/comments. But also nice to have if it would work when getting all book objects.
You can use the positional $ operator and filter directly from find. As greatly explaned by #Blakes Seven Here
BookSchema.find({
'comments.is_deleted': false,
}, {
'comments.$': 1,
});
EDIT:
As you said the $ operator only return one element, here is what the document says :
The positional $ operator limits the contents of an from the
query results to contain only the first element matching the query
document
There is two solution to your problem:
Make an aggregate, which is usually slow to get executed by database
Get the books and filter the comments using loops
BookSchema.find({
'comments.is_deleted': false,
}).map(x => Object.assign(x, {
comments: x.comments.filter(y => !y.is_deleted),
}));
The find get all books that have a non-deleted comment.
The map loop on each book.
We then remove the comments marked as deleted

How to insert data into subdocument using $push in query, instead of retrieving doc and saving it back

Edit: this was actually working
As the Mongoose - Subdocs: "Adding subdocs" documentation says, we can add a subdoc using the push method (i.e. parent.children.push({ name: 'Liesl' });)
But I want to go further, and would like to use the $push operator to insert subdocuments.
I have two Schemas: the ThingSchema:
var ThingSchema = mongoose.Schema({
name: {
type: String,
required: true
},
description: {
type: String
}
});
and the BoxSchema, the main document that has an array of subdocuments (things) of ThingSchema:
var BoxSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
description: {
type: String
},
things: {
type: [ThingSchema]
}
});
var BoxModel = mongoose.model('Box', BoxSchema);
I need every subdocument in things to have unique names - that is, that it would be impossible to insert a new document into this array that has a name value that already exists in the subdocs.
I'm trying to do something like:
var thingObj = ... // the 'thing' object to be inserted
BoxModel.update({
_id: some_box_id, // a valid 'box' ObjectId
"things.name": { "$ne": thingObj.name }
},
{
$push: { things: thingObj}
},
function(err) {
if (err) // handle err
...
});
but not getting any desired results.
What would be the correct way to add a ThingSchema subdocument into BoxSchema's thing array using the $push operator to do so in the query (must not add the subdoc if there's another subdoc named the same), instead of the Mongoose Docs way?
Edit: this is actually the issue
I made a mistake, the code above works as expected but now the problem I have is that when thingObj does not match the ThingSchema, an empty object is inserted into the things array:
// now thingObj is trash
var thingObj = { some: "trash", more: "trash" };
When executing the query given the above trash object, the following empty object is inserted into the subdocs array:
{ _id: ObjectId("an_obj_id") }
What I want this case, when the thingObj doesn't match the ThingSchema, is nothing to be added.
$addToSet adds something unique to the array (as in it checks for duplicates). But it only works for primitives.
What you should do is put things into their own collection and make a unique index on name. Then, make this change
things: {
type: [{type: ObjectId, ref: 'thingscollection'}]
}
this way you can do
BoxModel.update({
_id: some_box_id, // a valid 'box' ObjectId
"things": { "$ne": thingObj._id }
},
{
$addToSet: { things: thingObj._id}
},
function(err) {
if (err) // handle err
...
});
And when you fetch use .populate on things to get the full documents in there.
It's not exactly how you want it, but that's a design that might achieve what you're aiming for.

Mongoose field how to auto increment

I have a mongoose Schema like below:
var userSchema = new Shema({
userName: {type: String, default: "ABC"},
lastLoginTime: {type: Date, default: Date.now},
loginTimes: {type: Number, default: 1},
......
});
And I want to update lastLoginTime and let loginTimes plus one when user login every time.
It's easy to update lastLoginTime, just give it a new time string.
But how can I make loginTimes plus one every time.
Well you can always use the $inc operator with a form of "update"
Model.update(
{ _id: docId },
{
"$set": { "lastLoginTime": new Date() },
"$inc": { "loginTimes": 1 }
},
function(err,numaffected) {
}
)
That is a general MongoDB preferred way of doing things as there is minimal traffic sent for each actual update.
Also see the .findByIdAndUpdate() method for mongoose since this is likely restricted to one document.
An alternate is to use "pre save" hooks to be very mongoose about it, but it seems sort of cumbersome to me to retrieve and modify a document when you don't really need things like validation for this sort of update.

How to sort array of embedded documents via Mongoose query?

I'm building a node.js application with Mongoose and have a problem related to sorting embedded documents. Here's the schema I use:
var locationSchema = new Schema({
lat: { type: String, required: true },
lon: { type: String, required: true },
time: { type: Date, required: true },
acc: { type: String }
})
var locationsSchema = new Schema({
userId: { type: ObjectId },
source: { type: ObjectId, required: true },
locations: [ locationSchema ]
});
I'd like to output the locations embedded in the userLocations documented sorted by their time attribute. I currently do the sorting in JavaScript after I retrieved the data from MongoDb like so:
function locationsDescendingTimeOrder(loc1, loc2) {
return loc2.time.getTime() - loc1.time.getTime()
}
LocationsModel.findOne({ userId: theUserId }, function(err, userLocations) {
userLocations.locations.sort(locationsDescendingTimeOrder).forEach(function(location) {
console.log('location: ' + location.time);
}
});
I did read about the sorting API provided by Mongoose but I couldn't figure out if it can be used for sorting arrays of embedded documents and if yes, if it is a sensible approach and how to apply it to this problem. Can anyone help me out here, please?
Thanks in advance and cheers,
Georg
You're doing it the right way, Georg. Your other options are either to sort locations by time upon embedding in the first place, or going the more traditional non-embedded route (or minimally embedded route so that you may be embedding an array of ids or something but you're actually querying the locations separately).
This also can be done using mongoose sort API as well.
LocationsModel.findOne({ userId: theUserId })
// .sort({ "locations.time": "desc" }) // option 1
.sort("-locations.time") // option 2
.exec((err, result) => {
// compute fetched data
})
Sort by field in nested array with Mongoose.js
More methods are mentioned in this answer as well
Sorting Options in mogoose
Mongoose Sort API

Resources