Mongoose field match populate - node.js

I'm having some trouble with how best to implement this join/query in mongoose.
var mongoose = require('mongoose');
var addressSchema = new mongoose.Schema{
rank: Number,
address: String,
tag_name: String,
tag_url: String};
var tagSchema = new mongoose.Schema{
address: String,
name: String,
url: String};
I have a bunch of addresses saved and a bunch of tags saved. Some addresses have tags, most do not. I update the addresses and tags separately frequently. What I want to do is query some specific addresses and return them as an array with the tag fields filled in (the address tag fields are blank in the database).
So for example, I want to do this without making a db query for every address(101 db queries in the example). I'm not sure if $match or $in or populate is what I'm looking for. The below code is untested and may not work, but it should give you an idea of what I'm trying to do.
var db = require('../config/dbschema');
// find an array of addresses
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
// now fill in the tag fields and print
addTagField(0, addresses);
});
// recursive function to fill in tag fields
// address tag name and tag url fields are blank in the database
function addTagField(n, addresses) {
if(n < addresses.length) {
db.tagModel.find( { address: addresses[n].address }, function(err, tag) {
// if find a tag with this address, fill in the fields
// otherwise leave blank and check next address in array
if(tag) {
addresses[n].tag_name = tag.name;
addresses[n].tag_url = tag.url;
}
addTagField(n+1, addresses);
});
} else {
console.log(addresses);
}
}
http://mongoosejs.com/docs/api.html#aggregate_Aggregate-match
http://mongoosejs.com/docs/api.html#query_Query-in
http://mongoosejs.com/docs/api.html#document_Document-populate
I want to do what the above does with fewer db queries.

Your major problem is that you're not taking advantage of Mongoose's relationship mapping. Change your schemas just a bit and your problem will easily be solved. You can do it like this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [tagSchema],
})
addressModel.find({rank: {$lte: 100}}, function(err, addresses) {
...
})
or this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [{type: ObjectId, ref: 'Tag'}],
})
addressModel
.find({rank: {$lte: 100}})
.populate('tags', 'name url')
.exec(function(err, addresses) {
...
})

I didn't want to embed the docs. The is what I came up with.
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
if(err) return res.send(400);
if(!addresses) return res.send(404);
var addrOnlyAry = addresses.map(function(val, idx) { return val.address; });
db.tagModel.find( { address: { $in: addrOnlyAry } }, {}, function(err, tags) {
if(err) return res.send(400);
if(tags.length > 0) addresses = setTagFields(addresses, tags);
return res.json(addresses);
}
}
function setTagFields(addresses, tags) {
for(var i=0; i < tags.length; i++) {
for(var j=0; j < addresses.length; j++) {
if(addresses[j].address === tags[i].address) {
addresses[j].tag_name = tags[i].tag;
addresses[j].tag_url = tags[i].url;
break;
}
}
}
return addresses;
}

Related

How do I create a number of objects with insertMany with existing data from the db?

I'm currently trying to insert a large number of models through insertMany, but I can't seem to figure out how to populate the array when creating an object. I'm relatively new to Mongoose and any help would be appreciated, here is the code I have right now.
const ProgramsSchema = new mongoose.Schema({
program_id: {
type: String,
required: true
},
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: {
type: [{type: ProgramsSchema, ref: "Programs"}]
}
});
And here's the code where I try to create a number of schools and add it to the database.
let new_schools = []
for (let i = 0; i < schools.length; i++) {
let school = schools[i]
let p_arr = []
for (let p_index = 0; p_index < school["PROGRAMS"].length; p_index++) {
let p_id = school["PROGRAMS"][p_index]
Programs.find({program_id: p_id}).populate('Programs').exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0])
}
})
}
let newSchool = {
inst_url: school["INSTURL"],
programs: p_arr,
}
new_schools.push(newSchool);
}
Schools.insertMany(new_schools);
I can basically add all of the school data into the db, but none of the programs are being populated. I was wondering if there was a way to do this and what the best practice was. Please let me know if you guys need more info or if my question wasn't clear.
There are a few problems with your mongoose schemas. The operation you are trying to do in find is not available, based on your mongoose schemas. You cannot populate from "Programs" to "Schools". You can populate from "Schools" to "Programs", for instance:
Schools.find().populate(programs)
And to do that, several changes in your schemas are necessary. The idea is to store the programs _id in your programs array in School collection and be able to get the programs info through populate(), either regular populate or 'custom populate' (populate virtuals).
Regular populate()
I would change the schoolsSchema in order to store an array of _id into programs:
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
You should change ProgramsSchema as well:
const ProgramsSchema = new mongoose.Schema({
_id: Schema.Types.ObjectId, // that's important
description: {
type: String
},
});
And now, you can do:
Programs.find({_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0]._id)
}
})
Your documents should be inserted correctly. And now you can populate programs when you are performing a query over School, as I indicated above:
Schools.find().populate(programs)
Populate Virtual
The another way. First of all, I have never tried this way, but I think it works as follows:
If you want to populate over fields that are not ObjectId, you can use populate virtuals (https://mongoosejs.com/docs/populate.html#populate-virtuals). In that case, your schemas should be:
const ProgramsSchema = new mongoose.Schema({
program_id: String,
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
Enable virtual in your School schema:
Schools.virtual('programs', {
ref: 'Programs',
localField: 'programs',
foreignField: 'program_id'
});
Then, you should store the program_id.
Programs.find({program_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0].program_id)
}
})
And as before, you can populate() when you need.
I hope I helped

How do I create a compound unique autoincrementing field in mongoose?

I would like to create a schema where I can set a specific name for a user, for example a display name that many people can have, while also allowing people to add them based on that name. My solution is to save that name into a model with two fields, one of which is the name itself, the second one having an id that represents the number of times that name is being used currently.
var DisplayName = new Schema({
account: {
type: ObjectId,
ref: 'Account',
},
name: {
type: String,
trim: true,
required: true,
},
number: {
type: Number,
required: true,
}
});
is what I have, but I would like to be able to just say
var displayName = new DisplayName({
name: 'name',
account: req.user.id
});
displayName.save();
and it would automatically set the field for me?
EDIT: I realize there wasn't enough detail in the original question:
I'd like to use mongoose's built-in post-save handlers to generate this field for me on creation. It shouldn't be updated after its created, only set the first time.
You can use a function as shown below to do that thing.
function saveDisplayName(schema) {
var query = DisplayName.find({name: schema.name});
query.then(function(results) {
var userNumber = 1;
if (results.length < 1) {
return userNumber;
}
for (var i = 0; i <= results.length - 1; i++) {
var result = results[i];
if (userNumber <= result.number) {
userNumber = result.number + 1;
}
if (i == results.length - 1) {
return userNumber;
}
}
}).then(function(userNumber) {
schema.number = userNumber;
var displayName = new DisplayName(schema);
return displayName.save();
}).then(function(data) {
console.log('Data saved', data)
}).then(null, console.log);
}

Search for an array match in MongoDB

I have a schema like this in Mongo DB:
MAIN:
var mySchema= new mongoose.Schema({
username: { type: String, unique: true },
custom_schema: [mongoose.modelSchemas.Custom]
});
app.db.model('Main', mySchema);
My Custom schema looks like this:
var custom_schema = new mongoose.Schema({
my_string:{type:String, default: '' },
somefield: {
//
}
});
app.db.model('Custom', custom_Schema);
I need to retrive all records from the data base whose custom_schema contains a specific string in the string my_string.
I am doing this right now:
var filters = {};
filters.somefield=new RegExp('^.*?'+city+'.*$', 'i');
req.app.db.models.Main.pagedFind({
filters: filters,
keys: 'mykeys',
limit: 1000,
page: 1,
sort: '_by something'
}, function(err, results) {
if (err) {
return next(err);
}
});
How do I add a filter to search for a particular string in the array of custom_Schema?
To search inside object in mongodb, you need to use same directives as you do in regular Object object.var, so your filters should look like filters['custom_schema.somefield'] = ...

Setting a virtual field in a Model based on an async query from another model

I want to have a user setting (in a user model) that is derived from the sum of values in another model.
What I have tried to do is create a virtual value using a query like this:
var schemaOptions = {
toObject: {
virtuals: true
}
,toJSON: {
virtuals: true
}
};
/**
* User Schema
*/
var UserSchema = new Schema({
firstname: String,
lastname: String,
email: String,
username: String,
provider: String,
phonenumber: Number,
country: String,
emailverificationcode: {type:String, default:'verifyme'},
phoneverificationcode: {type:Number, default:4321 },
emailverified: {type:Boolean, default:false},
phoneverified: {type:Boolean,default:false},
}, schemaOptions)
UserSchema
.virtual('credits')
.get(function(){
//Load Credits model
var Credit = mongoose.model('Credit');
Credit.aggregate([
{ $group: {
_id: '5274d0e5a84be03f42000002',
currentCredits: { $sum: '$amount'}
}}
], function (err, results) {
if (err) {
return 'N/A'
} else {
return results[0].currentCredits.toString();
//return '40';
}
}
);
})
Now, this gets the value but it fails to work correctly (I cannot retrieve the virtual 'value' credits). I think this is because of the async nature of the call.
Can someone suggest the correct way to achieve this?
Once again many thanks for any input you can provide.
Edit:
So I am trying to follow the suggested way but no luck so far. I cannot get my 'getCredits' method to call.
Here is what I have so far:
UserSchema.method.getCredits = function(cb) {
//Load Credits model
var Credit = mongoose.model('Credit');
Credit.aggregate([
{ $group: {
_id: '5274d0e5a84be03f42000002',
currentCredits: { $sum: '$amount'}
}}
], function (err, results) {
cb(results);
}
);
};
var User = mongoose.model('User');
User.findOne({ _id : req.user._id })
.exec(function (err, tempuser) {
tempuser.getCredits(function(result){
});
})
Any ideas? Thanks again
There are a few issues with your implementation:
UserSchema.method.getCredits
^^^^^^ should be 'methods'
Also, you have to make sure that you add methods (and virtuals/statics) to your schema before you create the model, otherwise they won't be attached to the model.
So this isn't going to work:
var MySchema = new mongoose.Schema(...);
var MyModel = mongoose.model('MyModel', MySchema);
MySchema.methods.myMethod = ... // too late, model already exists
Instead, use this:
var MySchema = new mongoose.Schema(...);
MySchema.methods.myMethod = ...
var MyModel = mongoose.model('MyModel', MySchema);
I would also advise you to always check/propagate errors.

How to Make Mongoose's .populate() work with an embedded schema/subdocument?

I read up that you can make Mongoose auto pouplate ObjectId fields. However I am having trouble structuring a query to populate fields in a subdoc.
My models:
var QuestionSchema = new Schema({
question_text: String,
type: String,
comment_field: Boolean,
core_question: Boolean,
identifier: String
});
var SurveyQuestionSchema = new Schema({
question_number: Number,
question: {type: Schema.Types.ObjectId, ref: 'Question', required: true} //want this popuplated
});
var SurveySchema = new Schema({
start_date: Date,
end_date: Date,
title: String,
survey_questions: [SurveyQuestionSchema]
});
Right now I achieve the effect by doing:
Survey.findById(req.params.id, function(err, data){
if(err || !data) { return handleError(err, res, data); }
var len = data.survey_questions.length;
var counter = 0;
var data = data.toJSON();
_.each(data.survey_questions, function(sq){
Question.findById(sq.question, function(err, q){
sq.question = q;
if(++counter == len) {
res.send(data);
}
});
});
});
Which obviously is a very error-prone way of doing it...
As I noted in the comments above, this is an issue currently under scrutiny by the mongoose team (not yet implemented).
Also, looking at your problem from an outsider's perpsective, my first thought would be to change the schema to eliminate SurveyQuestion, as it has a very relational db "join" model feel. Mongoose embedded collections have a static sort order, eliminating the need for keeping a positional field, and if you could handle question options on the Survey itself, it would reduce the schema complexity so you wouldn't need to do the double-populate.
That being said, you could probably reduce the queries down to 2, by querying for all the questions at once, something like:
Survey.findById(req.params.id, function(err, data){
if(err || !data) { return handleError(err, res, data); }
var data = data.toJSON();
var ids = _.pluck(data.survey_questions, 'question');
Question.find({_id: { $in: ids } }, function(err, questions) {
_.each(data.survey_questions, function(sq) {
sq.question = _.find(questions, function(q) {
var id = q._id.toString();
return id == sq.question;
});
});
res.send(data);
});
});

Resources