I have a test schema of mongodb implemented in mongoose.
var TestSchema = new mongoose.Schema({ exam:[ Exam ] });
var ExamSchema = mongoose.Schema({type:String, questions: [ { question:{ type: ObjectId, ref: 'Question' }, answer:String } ] });
var QuestionSchema = mongoose.Schema({ desciption:String, solution:String });
The idea of the test, is a student might participate a test of several exams, each exam has a type name (could be Math or Physics ) and a list of question ObjectID as well as the corresponding answer filled by the student.
This code could help to add new question and answer to certain exam in the test
TestModel.update({'_id':pid,'exam.type':type},{'$push':{'exam.$.questions':{'question':questionsId,'answer':answer}}},options,function(err,ref){
if(err) {
console.log('add question to Exam'.red,err);
callback(err, null);
}else{
console.log('add question to Exam'.green+ref);
callback(null,ref);
}
})
It works well by adding but it comes to removing a question and answer, the update doesn't work.
Model.update({'_id':pid,'exam.type':type},{'$pull':{'exam.$.questions':questionId}},options,function(err,ref)
Model.update({'_id':pid,'exam.type':type},{'$pull':{'exam.$.questions.question':questionId}},options,function(err,ref)
Model.update({'_id':pid,'exam.type':type,'exam.questions.question':questionId},{'$pull':{'exam.$.questions.$.question':questionId}},options,function(err,ref)
Model.update({'_id':pid,'exam.type':type,'exam.questions.question':questionId},{'$pull':{'exam.questions.$.question':questionId}},options,function(err,ref)
I tried these methods, but none of these work
To use $ operator in the next modifier:
{'$pull': {'exam.$.questions': questionId}
You must at first use $elemMatch: operator in your query:
{'_id': pid, exam: { $elemMatch: {type: type} } }
There is a mongo syntax answer someone else may provide.
One aspect of meteor I love is that you get to use javascript/coffeescript everywhere. I humbly suggest you extend that strategy to your use of mongo updates. I find myself just using my json/object manipulation abilities directly and $set the whole thing, rather than learning another syntax. Some would say it is premature optimization to limit the fields you retrieve until proven it would have an effect, so you probably retrieve the data anyway.
Related
I'm trying to create a custom query method in mongoose - similar to the populate()-function of mongoose. I've the following two simple schemas:
const mongoose = require('mongoose')
const bookSchema = new mongoose.Schema({
title: String,
author: {type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Author'}
}, {versionKey: false})
const authorSchema = new mongoose.Schema({
name: String
}, {versionKey: false})
Now, I want retrieve authors information and furthermore the books written by the author. As far as I know, mongoose provides custom queries, hence my idea was to write a custom query function like:
authorSchema.query.populateBooks = function () {
// semi-code:
return forAll(foundAuthors).findAll(books)
}
Now, to get all authors and all books, I can simply run:
authorModel.find({}).populateBooks(console.log)
This should result in something like this:
[ {name: "Author 1", books: ["Book 1", "Book 2"]}, {name: "Author 2", books: ["Book 3"] } ]
Unfortunately, it doesn't work because I don't know how I can access the list of authors selected previously in my populateBooks function. What I need in my custom query function is the collection of the previous-selected documents.
For example, authorModel.find({}) already returns a list of authors. In populateBooks() I need to iterate through this list to find all books for all authors. Anyone know how I can access this collection or if it's even possible?
populate: "Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s)" (from the docs i linked).
Based on your question, you're not looking for population. yours is a simple query (the following code is to achieve the example result you gave at the end. note that your books field had a value of an array of strings, I'm assuming those were the titles). Also, do note that the following code will work with the models you've already provided, but this is a bad implementation that i recommend against - for multiple reasons: efficiency, elegance, potential errors (for e.g, authors with identical names), see note after code:
Author.find({}, function(err, foundAuthors){
if (err){
console.log(err); //handle error
}
//now all authors are objects in foundAuthors
//but if you had certain search parameters, foundAuthors only includes those authors
var completeList = [];
for (i=0;i<foundAuthors.length;i++){
completeList.push({name: foundAuthors[i].name, books: []});
}
Book.find({}).populate("author").exec(function(err, foundBooks){
if (err){
console.log(err); //handle err
}
for (j=0;j<foundBooks.length;j++){
for (k=0;k<completeList.length;k++){
if (completeList[k].name === foundBooks[j].author.name){
completeList[k].books.push(foundBooks[j].title);
}
}
}
//at this point, completeList has exactly the result you asked for
});
});
However, as i stated, i recommend against this implementation, this was based on the code you already provided without changing it.
I recommend changing your author schema to include a books property:
var AuthorSchema = new mongoose.Schema({
books: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Book"
}]
//all your other code for the schema
});
And add all books to their respective authors. This way, all you would need to do to get an array of objects, each of which contains an author and all of his books is one query:
Author.find({}).populate("books").exec(function(err, foundAuthors){
//if there's no err, then foundAuthors is an array of authors with their books
});
That is far simpler, more efficient, more elegant and more effective than the earlier possible solution i gave, based on your already existing code without changing it.
I've got a complex Mongoose population issue that I'm trying to sort out, and wondered if someone could shed some light (yeah yeah, I know, could use a RDBMS, but most the other bits of the schema lend themselves nicely to Mongo).
I've got two models: a Study and a Participant.
Study:
var StudySchema = new mongoose.Schema({
name: String,
checklist: [
{
order: Number,
text: String
}
]
});
Participant:
var ParticipantSchema = new mongoose.Schema({
name: String,
checklist_items: [
{
isComplete: Boolean,
item: {
type: Schema.Types.ObjectId
}
}
]
});
When a participant is created (they're always part of a study), the checklist is copied over onto the participant, so we can keep track of that checklist on the individual participant. I'm simply pushing IDs into the Participant.checklist_items.item to link those back to the items on the Study. (These are referenced, not wholesale copied, so that text changes to the study checklist are propagated down naturally)
I want to populate this model when retrieving a participant. When I get them, I want item on checklist_items to be populated with the corresponding item from the study. Hope that makes sense.
I've tried things like:
Participant.findById(req.params.id)
.populate({path: 'checklist_items.item', populate: {model: 'Study', path: 'checklist'})
.exec()
But no dice. I've monkeyed around with this for awhile, and I'm not sure I'm grokking how to do this child-to-child type population.
Any ideas? Is this possible?
Edit: clarified title with correct terms
It appears this isn't possible with Mongoose, and represents a bit of antipattern. Leaving a reference to this issue for folks with this question in the future: https://github.com/Automattic/mongoose/issues/2772
You can simply try this:
Participant.findById(req.params.id)
.populate({path: 'checklist_items.item', model: 'Study'})
.exec()
This will fetch you requiredparticipant and populate all the item inside checklist_items.
See if it works for you.
I have this Mongoose schema:
var postSchema = mongoose.Schema({
postId: {
type: Number,
unique: true
},
upvotes: [
{
type: Number,
unique: true
}
]
});
what the best query to use to get the length of the upvotes array? I don't believe I need to use aggregation because I only want to query for one model, just need the length of the upvotes array for a given model.
Really struggling to find this info online - everything I search for mentions the aggregation methodology which I don't believe I need.
Also, as a side note, the unique schema property of the upvotes array doesn't work, perhaps I am doing that wrong.
find results can only include content from the docs themselves1, while aggregate can project new values that are derived from the doc's content (like an array's length). That's why you need to use aggregate for this, even though you're getting just a single doc.
Post.aggregate([{$match: {postId: 5}}, {$project: {upvotes: {$size: '$upvotes'}}}])
1Single exception is the $meta projection operator to project a $text query result's score.
I'm not normally a fan of caching values, but it might be an option (and after finding this stackoverflow answer is what I'm going to do for my use case) to calculate the length of the field when the record is updated in the pre('validate') hook. For example:
var schema = new mongoose.Schema({
name: String,
upvoteCount: Number,
upvotes: [{}]
});
schema.pre('validate', function (next) {
this.upvoteCount = this.upvotes.length
next();
});
Just note that you need to do your updates the mongoose way by loading the object using find and then saving changes using object.save() - don't use findOneAndUpdate
postSchema.virtual('upvoteCount').get(function () {
return this.upvotes.length
});
let doc = await Post.findById('foobar123')
doc.upvoteCount // length of upvotes
My suggestion would be to pull the entire upvotes fields data and use .length property of returned array in node.js code
//logic only, not a functional code
post.find( filterexpression, {upvote: 1}, function(err, res){
console.log(res.upvotes.length);
});
EDIT:
Other way of doing would be stored Javascript. You can query the
upvote and count the same in mongodb side stored Javascript using
.length
I'm not sure how to even phrase this question... but here is a try. I'm calling the Book the "Parent" model and the Author the "Child" model.
I have two mongoose models--- Author and Books:
var Author = mongoose.model("Author", {
name: String
});
var Book = mongoose.model("Book", {
title: String,
inPrint: Boolean,
authors: [ { type: mongoose.Schema.ObjectId, ref: "Author"} ]
});
I am trying to run a query which would return all of the authors (child model) who have books (parent model) which are inPrint.
I could think of ways to do it with multiple queries, but I'm wondering if there is a way to do it with one query.
You could use populate as stated in the docs
There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where population comes in. Read more about how to include documents from other collections in your query results here.
In your case, it would look something like this:
Book.find().populate('authors')
.where('inPrint').equals(true)
.select('authors')
.exec(function(books) {
// Now you should have an array of books containing authors, which can be
// mapped to a single array.
});
I just stumbled upon this problem today and solved it:
Author.find()
.populate({ path: 'books', match: { inPrint: true } })
.exec(function (err, results) {
console.log(results); // Should do the trick
});
The magic occurs in the match option of populate, which refers to a property of the nested document to populate.
Also check my original post
EDIT: I was confusing books for authors, now it's corrected
Here is my Mongoose Schema:
var SchemaA = new Schema({
field1: String,
.......
fieldB : { type: Schema.Types.ObjectId, ref: 'SchemaB' }
});
var SchemaB = new Schema({
field1: String,
.......
fieldC : { type: Schema.Types.ObjectId, ref: 'SchemaC' }
});
var SchemaC = new Schema({
field1: String,
.......
.......
.......
});
While i access schemaA using find query, i want to have fields/property
of SchemaA along with SchemaB and SchemaC in the same way as we apply join operation in SQL database.
This is my approach:
SchemaA.find({})
.populate('fieldB')
.exec(function (err, result){
SchemaB.populate(result.fieldC,{path:'fieldB'},function(err, result){
.............................
});
});
The above code is working perfectly, but the problem is:
I want to have information/properties/fields of SchemaC through SchemaA, and i don't want to populate fields/properties of SchemaB.
The reason for not wanting to get the properties of SchemaB is, extra population will slows the query unnecessary.
Long story short:
I want to populate SchemaC through SchemaA without populating SchemaB.
Can you please suggest any way/approach?
As an avid mongodb fan, I suggest you use a relational database for highly relational data - that's what it's built for. You are losing all the benefits of mongodb when you have to perform 3+ queries to get a single object.
Buuuuuut, I know that comment will fall on deaf ears. Your best bet is to be as conscious as you can about performance. Your first step is to limit the fields to the minimum required. This is just good practice even with basic queries and any database engine - only get the fields you need (eg. SELECT * FROM === bad... just stop doing it!). You can also try doing lean queries to help save a lot of post-processing work mongoose does with the data. I didn't test this, but it should work...
SchemaA.find({}, 'field1 fieldB', { lean: true })
.populate({
name: 'fieldB',
select: 'fieldC',
options: { lean: true }
}).exec(function (err, result) {
// not sure how you are populating "result" in your example, as it should be an array,
// but you said your code works... so I'll let you figure out what goes here.
});
Also, a very "mongo" way of doing what you want is to save a reference in SchemaC back to SchemaA. When I say "mongo" way of doing it, you have to break away from your years of thinking about relational data queries. Do whatever it takes to perform fewer queries on the database, even if it requires two-way references and/or data duplication.
For example, if I had a Book schema and Author schema, I would likely save the authors first and last name in the Books collection, along with an _id reference to the full profile in the Authors collection. That way I can load my Books in a single query, still display the author's name, and then generate a hyperlink to the author's profile: /author/{_id}. This is known as "data denormalization", and it has been known to give people heartburn. I try and use it on data that doesn't change very often - like people's names. In the occasion that a name does change, it's trivial to write a function to update all the names in multiple places.
SchemaA.find({})
.populate({
path: "fieldB",
populate:{path:"fieldC"}
}).exec(function (err, result) {
//this is how you can get all key value pair of SchemaA, SchemaB and SchemaC
//example: result.fieldB.fieldC._id(key of SchemaC)
});
why not add a ref to SchemaC on SchemaA? there will be no way to bridge to SchemaC from SchemaA if there is no SchemaB the way you currently have it unless you populate SchemaB with no other data than a ref to SchemaC
As explained in the docs under Field Selection, you can restrict what fields are returned.
.populate('fieldB') becomes populate('fieldB', 'fieldC -_id'). The -_id is required to omit the _id field just like when using select().
I think this is not possible.Because,when a document in A referring a document in B and that document is referring another document in C, how can document in A know which document to refer from C without any help from B.