Reputation: 12029
Ok, here is what I am trying to do. I have a user schema which has a team key and an array as its value. For example:
user = {
team : Array
}
The values of the array are strings (team names). I have a team schema that has the team information, such as roster, state, etc. Here is my question. If my user has more than one team name in the array, how then would I aggregate all the data to one variable and then pass it to my view. I thought I could just use a for loop, but the view seems to render before the aggregation is done. Here is what I tried:
// if there is there is more than one team name in the user's team array
if( data[0].teams.length > 1 ){
for(var i = 0; i < data[0].teams.length; i++){
// for each team name in the user array
// find the corresponding team and add that data to the array called team
Team.find( { team : data[0].teams[i] }, function(err, data){
if(err){
throw err;
}
team.push(data);
});
}
// render the view
res.render('coach', {
user: req.user,
fname: self.fname,
lname: self.lname,
email: self.email,
phone: self.phone,
address: self.address,
state: self.state,
teams : [team]
});
}
Edit (added schemas)
User schema ( look in teams array, find corresponding teams, return data from team schema based on the name in the user schema teams array )
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: Array,
});
Team Schema:
var teamsSchema = new Schema({
coach: String,
state: String,
conference: String,
team: String,
roster: [{fname: String, lname: String, number: String}]
});
Upvotes: 0
Views: 1567
Reputation: 151072
Now that you have posted your schema It is clear what you are trying to do and also clear how to show your various approaches to do this much better.
The first and basic case is that you seem to have an array of "teams" containing the "String" value for the "team name" presumably stored on your user object. This appears to work for you and does have the advantage of having those names accessible as you retrieve the user.
The thing is that you are iterating over the results and issuing a .find()
for every element in the array, which is not the most efficient way. You can basically use the $in
operator with your query and existing data and get around what you probably tried to do in merging the full "team data" to the user object by calling .toObject()
to transform the mongoose document to an plain JavaScript object:
User.findById( req.user, function(err,user) {
var data = user.toObject();
Team.find({ "team": "$in": data.teams }, function(err,teams) {
data.teams = teams;
res.render( 'coach', data );
});
});
That is the simple approach, but really there are features available to mongoose that will sort of do this for you if you change your schema a little to reference the data in the Team
model. So you can then just fill in using .populate()
:
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: [{ type: ObjectId, ref: "Team" }]
});
Now that stores just the references _id
value and where to get it from. So now your query becomes this:
User.findById( req.user).populate("teams").exec(function(err,user) {
res.render("coach", user);
});
So what this does is basically the equivalent of the first example but using the _id
values and in a little cleaner way, and the user
object now has pulled in all of the data from the Team
schema matching the list of _id
values stored in your user array.
Of course you don't have the same immediate access to just the team name as you did before, but you can work around this by selecting the fields to populate with something like this:
User.findById( req.user).populate("teams","team").exec(function(err,user) {
res.render("coach", user);
});
So that would only pull in the "team" field from each object and is much the same as what you originally had as a result, albeit with actually two queries (under the hood).
Finally if you can live with the concept of a little data replication, then the most efficient way is to simply embed the data. While there is duplicated data being stored,the reading is the most efficient as it is a single query. If you don't need to regularly update that data, this may be worth consideration:
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
fname : { type: String, required: true },
lname : { type: String, required: true },
password : { type: String, required: true },
type : { type: String, required: true },
phone : String,
address : String,
state: String,
conference: String,
email : { type: String, required: true, index: { unique : true } },
teams: [teamSchema]
});
So with that data embedded you only need to retrieve the user
and the "teams" data would already be there:
User.findById( req.user, function(err,user) {
res.render("coach", user);
});
So those are a few approaches to take. All show that the looping of the array is not required, and that the number of queries you are actually issuing can be greatly reduced and even down to 1 if you can live with that.
Upvotes: 1
Reputation: 3889
Firstly, the find operations are asynchronous and although you are waiting for a response before pushing to the team
array you are not waiting for all responses before responding with res.render()
. Something like below should show what I mean:
// if there is there is more than one team name in the user's team array
if( data[0].teams ){
var teamArr = [];
var responses = 0;
for(var i=0; i<data[0].teams.length; i++){
// for each team name in the user array
// find the corresponding team and add that data to the array called team
Team.find( { team : data[0].teams[i] }, function(err, team){
if(err){
throw err;
}
responses ++;
teamArr.push(team);
// if all teams have been pushed then call render
if (responses == data[0].teams.length - 1) {
// render the view
res.render('coach', {
user: req.user,
fname: self.fname,
lname: self.lname,
email: self.email,
phone: self.phone,
address: self.address,
state: self.state,
teams : teamArr
});
}
});
}
}
Edit: I changed some naming to avoid potential naming clashes (two data variables for example)
Secondly, another option if you change your document structure, given you are using mongoose, if you stored references to the team document objects in your array instead of name strings you could use the mongoose populate method and populate the array before returning the whole document.
Update: Based on Schema provided you could use populate as follows:
var userSchema = new Schema({
username : { type: String, required: true, index: { unique: true } },
....
email : { type: String, required: true, index: { unique : true } },
teams: [{
type: Schema.Types.ObjectId, ref: 'teams'
}],
});
And then populate the document before returning it avoiding the for loop:
User.findOne({_id: userId})
.populate('teams')
.exec(function (err, user) {
if (err) return handleError(err);
res.render('coach', {
user: filterUser(user);
});
});
where user.teams
now contains a populated array of teams and filterUser()
returns a user object with only the properties you require;
Upvotes: 1