How to find a sub-document using Mongoose? - javascript

I'm trying to get a sub-document in my User collection using mongoose. I followed the Mongoose Sub Document on its official website. It's written that:
Each document has an _id. DocumentArrays have a special id method for looking up a document by its _id.
var doc = parent.children.id(id);
Here is my code:
exports.editAccount = function(req, res) {
var user = new User(req.user);
var newAccount = new Account(req.body);
console.log("Account:" + newAccount._id); // Gave me 53bf93d518254f880c000009
var account = user.accounts.id(newAccount._id);
console.log("Account" + account); // Never been printed
};
The console.log("Account" + account); has never been printed. I don't know what happen. I tried many different ways, however, still can't figure it out. Any help would be appreciated.
User collection:
{
"__v" : 1,
"_id" : ObjectId("53bcf3e6fbf5adf10c000001"),
"accounts" : [
{
"accountId" : "123456789",
"type" : "Saving account",
"balance" : 100,
"_id" : ObjectId("53bf93d518254f880c000009")
}
]
}
I

Not too sure how you have defined your Schema or basically even model instances, but really all you need is this:
var accountSchema = new Schema({
"accountId": String,
"type": { "type": String, "enum": ["Saving Account", "Checking Account"] },
"balance": { "type": Number, "default": 0 }
]);
var userSchema = new Schema({
"accounts": [accountSchema]
]);
var User = mongoose.model( "User", userSchema );
Then when you want to add an account to the User you just do, presuming you have input that matches the first variable declaration:
var input = {
"accountId": "123456789",
"type": "Savings Account",
};
User.findByIdAndUpdate(
userId,
{ "$push": { "accounts": input } },
function(err,user) {
// work with result in here
}
);
That does bypass things like validation and other hooks, but is more efficient in communicating with MongoDB.
If you really need the validation and/or other features then you and using a .find() variant and issuing a .save() method.
User.findById(userId,function(err,user) {
if (err) throw err; // or handle better
user.accounts.push( input );
user.save(function(err, user) {
// more handling
});
]);
And to modify the document then you are doing much the same. Either by the most efficient MongoDB way:
var input = {
accountId: "123456789",
amount: 100
};
User.findOneAndUpdate(
{ "_id": userId, "accounts.accountId": input.accountId },
{ "$inc": { "accounts.$.balance": input.amount } },
function(err,user) {
// handle result
}
);
Or again where you need the Mongoose hooks and or validation to apply:
User.findById(userId,function(err,user) {
if (err) throw err; // or handle otherwise
user.accounts.forEach(function(account) {
if ( account.accountId === input.accountId )
account.balance += input.balance;
});
user.save(function(err,user) {
// handle things
});
);
Remember that these things are "arrays", and you can either handle them the MongoDB way or the JavaScript way. It just depends on where you choose to "validate" your input.
More code to illustrate where the usage is not correct:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/child');
var accountSchema = new Schema({
"accountId": String,
"type": { "type": String },
"balance": { "type": Number, "default": 0 }
});
var userSchema = new Schema({
"accounts": [accountSchema]
});
var User = mongoose.model( "User", userSchema );
async.waterfall([
function(callback) {
User.create({},function(err,user) {
if (err) throw err;
console.log(
"Created:\n%s\n",
JSON.stringify( user, undefined, 4 )
);
callback(null,user);
});
},
function(user,callback) {
var account = user.accounts.create({
"accountId": "123456789",
"type": "Savings"
});
console.log(
"Account is:\n%s\n",
JSON.stringify( account, undefined, 4 )
);
console.log(
"User is still:\n%s\n",
JSON.stringify( user, undefined, 4 )
);
user.accounts.push( account );
console.log(
"User Changed:\n%s\n",
JSON.stringify( user, undefined, 4 )
);
User.findById(user.id,function(err,saved) {
if (err) throw err;
console.log(
"Persisted is still:\n%s\n",
saved
);
user.save(function(err,user) {
if (err) throw err;
callback(null,user,account);
});
});
},
function(user,account,callback) {
User.findById(user.id,function(err,saved) {
if (err) throw err;
console.log(
"Persisted is now:\n%s\n",
saved
);
var item = user.accounts.id(account.id);
console.log(
"Item is:\n%s\n",
item
);
callback();
});
}
],function(err) {
process.exit();
});
Results:
Created:
{
"__v": 0,
"_id": "53c08ab51083d1fe3852becc",
"accounts": []
}
Account is:
{
"accountId": "123456789",
"type": "Savings",
"_id": "53c08ab51083d1fe3852becd",
"balance": 0
}
User is still:
{
"__v": 0,
"_id": "53c08ab51083d1fe3852becc",
"accounts": []
}
User Changed:
{
"__v": 0,
"_id": "53c08ab51083d1fe3852becc",
"accounts": [
{
"accountId": "123456789",
"type": "Savings",
"_id": "53c08ab51083d1fe3852becd",
"balance": 0
}
]
}
Persisted is still:
{ _id: 53c08ab51083d1fe3852becc, __v: 0, accounts: [] }
Persisted is now:
{ _id: 53c08ab51083d1fe3852becc,
__v: 1,
accounts:
[ { accountId: '123456789',
type: 'Savings',
_id: 53c08ab51083d1fe3852becd,
balance: 0 } ] }
Item is:
{ accountId: '123456789',
type: 'Savings',
_id: 53c08ab51083d1fe3852becd,
balance: 0 }

Related

Mongoose mixed Type object array, can findOneAndUpdate

I have a controller edit card which updates the fields of cardsArray object.
cardsArray is mixed type object as fileds of each card object is different so i am storing mixed.type
Althought pushing new card using addCard controller works perfectly
But when edit card controller is called, it gives type error
When edit Controlller is callled is gives following error:
TypeError: Cannot read properties of null (reading 'cardsArray')
// Schema
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
cardsArray: [{ type: mongoose.Schema.Types.Mixed }],
}
);
//__Mongodb Data
{
"_id": {
"$oid": "63b43ab32fc8d3c100cafecc"
},
"name": "usern_name",
"email": "pr****#gmail.com",
"password": "$2b$12$3nwifHakrBu94BwLXAC4Nu16Kw0.xyW8vAIPTMSgY7cYttVklDIZq",
"cardsArray": [
{
"title": "some_title",
"category": "Bank",
"cardHolder": "some_name",
"cardNumber": "54545454",
"expiry": "23/01",
"cvv": "***",
"logoIndex": 72,
"isFavourite": false,
"_id": {
"$oid": "63b83cc77288d277ef359533"
}
}
],
"loginIdsArray": [],
"docsArray": [],
"activitiesArray": [],
"__v": 0
}
// Add Card Controller.js___
addCard: async (req, res) => {
console.log(req.body.data, req.body.user_id)
// console.log('obj_id', newObjectId)
req.body.data._id = newObjectId;
try {
const response = await UserDatabase.findOneAndUpdate(
{ _id: req.body.user_id },
{
$push: {
// cardsArray: req.body.data,
cardsArray: { $each: [req.body.data] },
},
},
{ returnOriginal: false }
);
res.status(200).send(response);
} catch (error) {
console.log(error)
res.status(404).json({ message: error.message });
}
},
// edit card controller.js
editCard: async (req, res) => {
const id = req.params.id;
const { category, title, cardHolder, cardNumber, expiry, cvv, logoIndex, isFavourite } = req.body;
console.log(req.params.id)
try {
const response = await UserDatabase.findOneAndUpdate(
{ _id: "63b43ab32fc8d3c100cafecc", 'cardsArray._id': "63b709fc69a1cfa6fccd645c" },
{
$set: {
"cardsArray.$.title": req.body.title,
"cardsArray.$.category": req.body.category,
"cardsArray.$.cardHolder": req.body.cardHolder,
"cardsArray.$.cardNumber": req.body.cardNumber,
"cardsArray.$.expiry": req.body.expiry,
"cardsArray.$.cvv": req.body.cvv,
"cardsArray.$.logoIndex": req.body.logoIndex,
"cardsArray.$.isFavourite": req.body.isFavourite
}
},
);
console.log(response)
res.status(201).json(response.cardsArray);
} catch (error) {
console.log(error)
res.status(404).json({ message: error.message });
}
}
it means that there is no data matching the following _id and cardsArray._id
i thinks so its fails to find the feild 'cardsArray._id',first try finding the value by just findOne
await UserDatabase.findOneAndUpdate(
{ _id: "63b43ab32fc8d3c100cafecc", 'cardsArray._id': "63b709fc69a1cfa6fccd645c" })
if your find doesn't work try below methord not sure but it may work you have to either find the id and loop through the cardsArray of use $in or $match
await UserDatabase.findOneAndUpdate(
{ _id: "63b43ab32fc8d3c100cafecc", 'cardsArray':{$in:[ {_id:"63b709fc69a1cfa6fccd645c" }]})

findOneAndUpdate (mongoose) returns true for updating a nested document in a model but nothing is updated

Yes, I know there are very similar other questions and I was looking at them, but, unfortunately, I could not find a solution. So, I am trying to update a nested document in a mongoose model with "findOneAndUpdate" method but even though it is returning a document, this one is the original one, not the updated.
Below is the model:
var mongoose = require('mongoose');
var teamMemberModelSchema = new mongoose.Schema({
"_id": mongoose.SchemaTypes.ObjectId,
"email": {
"type": String,
"required": true,
"min": 5,
"max": 20
},
"name": {
"type": String,
"required": true,
"min": 5,
"max": 20
},
"role": {
"type": String,
"required": true,
"min": 20,
"max": 50
},
"twitter": {
"type": String,
"required": true,
"min": 20,
"max": 50
},
"facebook": {
"type": String,
"required": true,
"min": 20,
"max": 50
},
"linkedin": {
"type": String,
"required": true,
"min": 20,
"max": 50
},
});
var teamModelSchema = new mongoose.Schema({
"title": {
"type": String,
"required": true,
"min": 5,
"max": 20
},
"headline": {
"type": String,
"required": true,
"min": 5,
"max": 30
},
"description": {
"type": String,
"required": true,
"min": 5,
"max": 80
},
"members": [teamMemberModelSchema]
}, { collection: 'team' });
teamModelSchema.set('collection', 'team');
mongoose.model('team', teamModelSchema);
I want to update a member nested document with this following code snippet:
module.exports.teamMemberUpdate = function (req, res) {
var email = req.body.email;
var name = req.body.name;
var role = req.body.role;
var twitter = req.body.twitter;
var facebook = req.body.facebook;
var linkedin = req.body.linkedin;
teamFunctions.findTeamMember(email)
.then(data => {
if (data.length === 0) {
responseUtilities.sendJSON(res, false, { "message": teamMsg.teamMemberNotFound });
}
else {
return data[0];
}
})
.then(() => {
const query = { "members.email": email };
const options = { new: true };
var update = {
$set: {
email: email,
name: name,
role: role,
twitter: twitter,
facebook: facebook,
linkedin: linkedin
}
};
TeamModel.findOneAndUpdate(query, update, options, function (err, result) {
var message = teamMsg.teamMemberUpdatedSuccess;
if (!result) {
message = teamMsg.teamMemberUpdatedError;
}
responseUtilities.sendJSON(res, err, { "message": message });
});
})
.catch(err => {
console.log(err.message);
responseUtilities.sendJSON(res, err, { "message": err.message });
});
}
"teamMemberUpdatedSuccess" value is displayed, but in fact, the original document is not updated.
Can anyone find out where the problem is?
You can use the positional $ operator with dot notation. Here is the docs for your scenario.
You can use the following function to update a member with a given email. As you see we have only one db access. There is no need to use teamFunctions.findTeamMember function.
I used some modern javascript features like object destructuring and async/await syntax for clean code.
module.exports.teamMemberUpdate = async function(req, res) {
const { email, name, role, twitter, facebook, linkedin } = req.body;
const query = {
"members.email": email
};
const options = { new: true };
const update = {
$set: {
"members.$.email": email,
"members.$.name": name,
"members.$.role": role,
"members.$.twitter": twitter,
"members.$.facebook": facebook,
"members.$.linkedin": linkedin
}
};
try {
const result = await TeamModel.findOneAndUpdate(query, update, options);
if (!result) {
return res.status(400).send("No member found for the email");
}
res.send(result);
} catch (err) {
console.log(err);
res.status(500).send("Something went wrong");
}
};
Please first try with this function without changing anything, after you see it works, you may make application specific changes.
Also I noticed a minor probem in your schema definition, you should use minlenght and maxlength options for String type instead of min and max.
TeamModel does not have any property called email, name etc (see your update variable). Your update query should look like this:
const update = {
$set: {
'members.$.email': email
}
};
That $ is important because members is an array.
Hope it helps.

Given this model, how to delete a nested array item with updateOne (mongoose)?

I am trying to remove an array item with "updateOne" method but my query is not matching the right record in the model structure that I have. Given an email, I would like to find the array item with the provided email and pulls it out, remove it. (There is no array item with the same email)
My model is like so:
var mongoose = require('mongoose');
var teamMemberModelSchema = new mongoose.Schema({
_id: false,
"email": {
"type": String,
"required": true,
"minlenght": 5,
"maxheight": 50
},
"name": {
"type": String,
"required": true,
"minlenght": 5,
"maxheight": 256
},
"role": {
"type": String,
"required": true,
"minlenght": 20,
"maxheight": 256
},
"twitter": {
"type": String,
"required": true,
"minlenght": 1,
"maxheight": 100
},
"facebook": {
"type": String,
"required": true,
"minlenght": 1,
"maxheight": 100
},
"linkedin": {
"type": String,
"required": true,
"minlenght": 1,
"maxheight": 100
},
});
var teamModelSchema = new mongoose.Schema({
"title": {
"type": String,
"required": true,
"minlenght": 5,
"maxheight": 20
},
"headline": {
"type": String,
"required": true,
"minlenght": 5,
"maxheight": 30
},
"description": {
"type": String,
"required": true,
"minlenght": 5,
"maxheight": 80
},
"members": [teamMemberModelSchema]
}, { collection: 'team' });
teamModelSchema.set('collection', 'team');
mongoose.model('team', teamModelSchema)
And the approach that I am trying is the following:
module.exports.removeMember = function (req, res) {
const email = req.params.email;
const query = { "members.email": email };
const pull = { $pull: { "members.$.email": email } };
try {
var message = teamMsg.teamMemberRemoveSuccess;
TeamModel.updateOne(query, pull);
responseUtilities.sendJSON(res, false, { message: message });
} catch (err) {
console.log(err.message);
responseUtilities.sendJSON(res, err, { message: err.message });
}
};
It executes with no errors, but nothing is updated.
I have tried some others alternatives with "FindOneAndUpdate" and "FindOneAndRemove" but, I could not find a solution.
Any ideas?
You can use findOneAndUpdate with $pull operator for this task.
For removing items from an array of documents you can check MongoDb docs
You need to use await or then block to query. I used await, and made the function asynchronous by adding async keyword. We also need empty query object.
I also added the new: true option, to return the updated object to check if the item is deleted.
You need to handle the case where no document matches, I added a TODO for you.
module.exports.removeMember = async function(req, res) {
const email = req.params.email;
const query = {};
const pull = {
$pull: {
members: {
email: email
}
}
};
const options = {
new: true
};
try {
var message = teamMsg.teamMemberRemoveSuccess;
const result = await TeamModel.updateOne(query, pull, options);
console.log(result);
if (!result) {
//TODO: return 400-Bad Request or 404 - Not Found
} else {
responseUtilities.sendJSON(res, false, { message: message });
}
} catch (err) {
console.log(err.message);
responseUtilities.sendJSON(res, err, { message: err.message });
}
};
give this query a try:
db.collection.update(
{ 'members.email': 'email#address' },
{ $pull: { members: { email: 'email#address' } } },
{ multi: true }
)
Try using the update() method and async/await:
module.exports.removeMember = async function (req, res) {
const email = req.params.email;
console.log(`email = ${email}`) // Make sure correct value is coming thru
const query = { "members.email": email };
const pull = { $pull: { "members.$.email": email } };
const options = { multi: true }
try {
var message = teamMsg.teamMemberRemoveSuccess;
await TeamModel.update( query, pull, options );
responseUtilities.sendJSON(res, false, { "message": message });
} catch (err) {
console.log(err.message);
responseUtilities.sendJSON(res, err, { "message": err.message });
}
};

Append inside Schemaless Array

I am new to MongoDB, so far playing around with it, confronted with a problem, here i am having a hard time when trying to append multiple objects inside Schema-Less Array.So far i tried $push to append multiple objects inside array but got a Mongo Error.
[MongoError: Can't use $push/$pushALL within non-array
i don't know why i am getting this error, when using $push with array
Schema:
EventTypeSchema = new Schema(){
type: String,
eventID: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
PersonSchema = new Schema(){
PersonID: {
type: Schema.Types.ObjectId,
ref: 'User'
}
Invitation: [ ] //Schema-less
}
In Controller i have Access to both EventType and Person Model
Controller:
exports.update = function(req,res){
var event = new EventType();
event.type = 'EVENT';
event.eventID = req.body.eventid;
var query = {'PersonID': req.body.personid};
var update = {$push:{'Invitation': event}};
Person.update(query,update,function(err,user){...})
};
for debugging purposes i tried to give Mixed type Schema for Array but didn't get it to work
PersonSchema = new Schema(){
PersonID: {
type: Schema.Types.ObjectId,
ref: 'User'
}
Invitation: [ {
type: Schema.Types.Mixed
} ]
}
When i removed $push on update then only whole event object is getting inside Invitation, the reason i created Schema-less array is because i am dealing with different type of invitation, here i just described about event invitation, otherwise there are different type of invitations i am dealing with like, User Invitation for request, Conference invitation, so there would combination of different objectId's, i think there should be the way to append to schema-less array in mongoDB.
EDIT:
The following is what I came up with. Not able to get it to work though.
function PortalTypes() {
Schema.apply(this,arguments);
this.add({
object_type: String,
});
}
util.inherits( PortalTypes, Schema );
var userType = new PortalTypes({
ID : {
type: Schema.Types.ObjectId,
ref : 'User'
}
});
var eventType = new PortalTypes({
ID : {
type: Schema.Types.ObjectId,
ref : 'events'
}
});
var user = new userType({ID:'dsaj3232--objectID','object_type':'user'});
user.save();
var event = new eventType({ID:'dasddehiqe98--objectID','object_type':'event'});
event.save();
Networks.Invitation.push(user,event);
How can I do something like this?
Despite your schema that error at the top means that that there is a matching document in the collection that does not have this field set as an array, but it's present with another type. Possibly just a string or object.
Here's a little, contrived example listing to demonstrate:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var personSchema = new Schema({
invitation: []
});
var Person = mongoose.model( 'Person', personSchema );
mongoose.connect('mongodb://localhost/test');
async.waterfall(
[
function(callback) {
Person.remove({},function(err,num) {
callback(err);
});
},
function(callback) {
console.log( "Creating" );
var person = new Person();
person.save(function(err,person) {
if (err) callback(err);
console.log(person);
callback(err,person);
});
},
function(person,callback) {
console.log( "Updating" );
Person.findOneAndUpdate(
{ "_id": person._id },
{ "$push": { "invitation": "something" } },
function(err,doc) {
if (err) callback(err);
console.log(doc);
callback(err);
}
);
},
function(callback) {
console.log( "Upserting" );
Person.findOneAndUpdate(
{ "name": "bob" },
{ "$set": { "invitation": {} } },
{ "upsert": true },
function(err,doc) {
if(err) callback(err);
console.log(doc);
callback(err,doc);
}
);
},
function(bob,callback) {
console.log( "Failing" );
Person.findOneAndUpdate(
{ "name": "bob" },
{ "$push": { "invitation": "else" } },
function(err,doc) {
if (err) callback(err);
console.log(doc);
callback(err);
}
);
}
],
function(err) {
if (err) throw err;
console.log( "Done" );
mongoose.disconnect();
}
);
That should give results like this:
Creating
{ __v: 0, _id: 54a18afb345b4efc02f21020, invitation: [] }
Updating
{ _id: 54a18afb345b4efc02f21020,
__v: 0,
invitation: [ 'something' ] }
Upserting
{ _id: 54a18afb9997ca0c4a7eb722,
name: 'bob',
__v: 0,
invitation: [ {} ] }
Failing
/home/neillunn/scratch/persons/node_modules/mongoose/lib/utils.js:413
throw err;
^
MongoError: exception: The field 'invitation' must be an array but is of type Object
in document {_id: ObjectId('54a18afb9997ca0c4a7eb722')}
The error message is a bit different since they were improved a bit in MongoDB 2.6 and upwards ( where this error string comes from ) to be a bit more precise about the actual problem. So in modern versions you would be told exactly what was wrong.
Despite the schema, methods like .update() ( I used .findOneAndUpdate() for convenience ) bypass the mongoose schema definition somewhat and go right to the database. So it's possible to do this and also possible you just had a document in place already, or otherwise created when a different schema definition was in place.
So that's the first problem here.
The rest of what you seem to be asking is for a "polymorphic" type of association in the array, and also where you do not wish to "embed" the whole created object in the array but just a reference to it.
Mongoose has "discriminators" to allow for this sort of thing, allowing different model types for objects to be stored within the same collection, but resolving to their own object and schema "type".
Following the current documentation example, here is an example listing of what that might look like:
var util = require('util'),
async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
function logger(label,content) {
console.log(
"%s:\n%s\n", label, JSON.stringify( content, undefined, 4 ) );
}
function BaseSchema() {
Schema.apply(this,arguments);
this.add({
name: String,
createdAt: { type: Date, default: Date.now }
});
}
util.inherits( BaseSchema, Schema );
var personSchema = new BaseSchema(),
bossSchema = new BaseSchema({ department: String });
var companySchema = new Schema({
people: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
var Person = mongoose.model( 'Person', personSchema ),
Boss = Person.discriminator( 'Boss', bossSchema ),
Company = mongoose.model( 'Company', companySchema );
mongoose.connect('mongodb://localhost/test');
async.waterfall(
[
function(callback) {
Company.remove({},function(err,num) {
callback(err);
});
},
function(callback) {
Person.remove({},function(err,num) {
callback(err);
});
},
function(callback) {
var person = new Person({ name: "Bob" });
person.save(function(err,person) {
logger("Person", person);
callback(err,person);
});
},
function(person,callback) {
var boss = new Boss({ name: "Ted", department: "Accounts" });
boss.save(function(err,boss) {
logger("Boss", boss);
callback(err,person,boss);
});
},
function(person,boss,callback) {
var company = new Company();
company.people.push(person,boss);
company.save(function(err,company) {
logger("Stored",company);
callback(err,company);
});
},
function(company,callback) {
Company.findById(company.id)
.populate('people')
.exec(function(err,company) {
logger("Polulated",company);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
Which will produce output like this:
Person:
{
"__v": 0,
"name": "Bob",
"createdAt": "2014-12-29T17:53:22.418Z",
"_id": "54a1951210a7a1b603161119"
}
Boss:
{
"__v": 0,
"name": "Ted",
"department": "Accounts",
"__t": "Boss",
"createdAt": "2014-12-29T17:53:22.439Z",
"_id": "54a1951210a7a1b60316111a"
}
Stored:
{
"__v": 0,
"_id": "54a1951210a7a1b60316111b",
"people": [
"54a1951210a7a1b603161119",
"54a1951210a7a1b60316111a"
]
}
Polulated:
{
"_id": "54a1951210a7a1b60316111b",
"__v": 0,
"people": [
{
"_id": "54a1951210a7a1b603161119",
"name": "Bob",
"__v": 0,
"createdAt": "2014-12-29T17:53:22.418Z"
},
{
"_id": "54a1951210a7a1b60316111a",
"name": "Ted",
"department": "Accounts",
"__v": 0,
"__t": "Boss",
"createdAt": "2014-12-29T17:53:22.439Z"
}
]
}
As you can see, there is a different structure for how Person and Boss are saved, notably the _t property as well as other defined properties for the different objects. Both however are actually stored in the same "people" collection and can be queried as such.
When storing these on the Company object, only the "reference id" values are stored in the array. Debatable to what you might want, but this is the difference between "referenced" and "embedded" schema models. You can see however when the .populate() method is called, then the objects are restored to their full form as they are read from the referenced collection.
So check your collection for existing documents that vary from your schema definition, and consider the approach as shown to represent a "polymorphic" association for different "types" of objects.
Note though that this kind of resolution is only supported under the "referenced" schema design, which can also possibly have it's drawbacks. If you want the objects stored as "embedded" within the single Company collection ( for example ), then you don't get the type of object resolution with varying schema types done by mongoose automatically. Resolving different types of objects would have to be done manually in your code, or provided plugin or however you do it.
More
Being specific to all of the purpose because there seems to be some confusion following something based on the standard documentation example, here is a more heavily commented listing:
var util = require('util'),
async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
// Utility
function logger(label,content) {
console.log(
"%s:\n%s\n", label,
util.inspect( content, false, 8, false ) );
}
/*
* Schemas:
*
* you can use a base schema for common fields or just a plain
* definition
*/
var portalSchema = new Schema(),
userSchema = new Schema({
"name": String,
"age": Number
}),
eventSchema = new Schema({
"place": String,
"eventDate": { type: Date, default: Date.now }
});
/*
* Models
*
* there is only one "model" defined and therefore one collection only
* as everything is comes from a stored __v field with the "Model" name
* defined in the discriminator
*/
var Portal = mongoose.model( 'Portal', portalSchema ),
User = Portal.discriminator( 'User', userSchema ),
Event = Portal.discriminator( 'Event', eventSchema );
/*
* Then there is the thing that is going to consume the references to the
* 'Portal' model. The array here references the "base" model.
*/
var otherSchema = new Schema({
"afield": String,
"portals": [{ type: Schema.Types.ObjectId, ref: "Portal" }]
});
var Other = mongoose.model( 'Other', otherSchema );
/*
* Meat:
*
* Let's start doing things
*/
mongoose.connect('mongodb://localhost/test');
// Just because we're passing around objects without globals or other scoping
async.waterfall(
[
// Start fresh by removing all objects in the collections
function(callback) {
Other.remove({},function(err,num) {
callback(err);
});
},
function(callback) {
Portal.remove({},function(err,num) {
callback(err);
});
},
// Create some portal things
function(callback) {
var eventObj = new Event({ "place": "here" });
eventObj.save(function(err,eventObj) {
logger("Event", eventObj);
callback(err,eventObj);
});
},
function(eventObj,callback) {
var userObj = new User({ "name": "bob" });
userObj.save(function(err,userObj) {
logger("User", userObj);
callback(err,eventObj,userObj);
});
},
// Store the references in the array for the Other model
function(eventObj,userObj,callback) {
var other = new Other({
"afield": "something"
});
other.portals.push(eventObj,userObj);
other.save(function(err,other) {
logger("Other Stored",other);
callback(err,other);
});
},
// See how it's all really stored
function(other,callback) {
Portal.find({},function(err,portals) {
logger("Portals",portals);
callback(err,other);
});
},
// But watch the magic here
function(other,callback) {
User.find({},function(err,portals) {
logger("Just Users!",portals);
callback(err,other);
});
},
// And constructed as one object by populate
function(other,callback) {
Other.findById(other.id)
.populate('portals')
.exec(function(err,other) {
logger("Other populated",other);
console.log("%s: %s",
"1st Element", other.portals[0].constructor.modelName );
console.log("%s: %s",
"2nd Element", other.portals[1].constructor.modelName );
callback(err);
});
}
],
function(err) {
// It's just a script, so clean up
if (err) throw err;
mongoose.disconnect();
}
);
That should explain some things and what "discriminators" are. Everything is stored in just "one" collection which is bound to the base model. Everything else is defined using .discriminator() from that base. The "name" of the "class model" or "discriminator" is stored on the object. But note that is stored on the collection only, not in the place where they are referenced as that only stores the _id values. Look at the output carefully:
Event:
{ __v: 0,
place: 'here',
__t: 'Event',
_id: 54a253ec456b169310d131f9,
eventDate: Tue Dec 30 2014 18:27:40 GMT+1100 (AEDT) }
User:
{ __v: 0,
name: 'bob',
__t: 'User',
_id: 54a253ec456b169310d131fa }
Other Stored:
{ __v: 0,
afield: 'something',
_id: 54a253ec456b169310d131fb,
portals: [ 54a253ec456b169310d131f9, 54a253ec456b169310d131fa ] }
Portals:
[ { _id: 54a253ec456b169310d131f9,
place: 'here',
__v: 0,
__t: 'Event',
eventDate: Tue Dec 30 2014 18:27:40 GMT+1100 (AEDT) },
{ _id: 54a253ec456b169310d131fa,
name: 'bob',
__v: 0,
__t: 'User' } ]
Just Users!:
[ { _id: 54a253ec456b169310d131fa,
name: 'bob',
__v: 0,
__t: 'User' } ]
Other populated:
{ _id: 54a253ec456b169310d131fb,
afield: 'something',
__v: 0,
portals:
[ { _id: 54a253ec456b169310d131f9,
place: 'here',
__v: 0,
__t: 'Event',
eventDate: Tue Dec 30 2014 18:27:40 GMT+1100 (AEDT) },
{ _id: 54a253ec456b169310d131fa,
name: 'bob',
__v: 0,
__t: 'User' } ] }
1st Element: Event
2nd Element: User
So there is only one collection for all "portal" types but there is some magic there as shown. The "others" collection only stores the _id values in it's array of "portals". This is how mongoose references work, where the "model" and attached schema is not stored in the data but as part of the code definition.
The "discriminator" part stores this "model name" on the field so it can be resolved to the correct type, but it's still all in the same collection, and part of the User model magic demonstrated.
Why? It's how .populate() works. Under the hood an $in operator is used with the array content, so it's all expected to be in the one place. But you can still resolve types as shown.
If you expect using separate collections, then you are doing everything manually and storing model names and querying other collections for references all by yourself.

MongoDB: geoNear not returning distance

I need to modify an existing geospatial query so that it includes the distance in the search results. (Both the document and the existing query are using legacy coordinate pairs.) The original query uses $near. Reading the MongoDB documentation it looks like geoNear should return distance, as is shown in several examples.
While I have been able to modify the query to use geoNear, the distances are not included in the search results. Here is an example of the new query:
{
geoNear: 'users',
near: [ '0', '0' ],
maxDistance: '90',
query: { userName: { '$regex': '^test' } }
}
One thing that's not clear to me is how Mongo ties the location to the specified in the query to the location of the document. In my case the users document has a field named lastKnownPosition. How does Mongo even know to query against that field?
Here's an example of the search results:
{
"__v" : 0 ,
"_id" : { "$oid" : "5413824f8b4d6f7505120a53"} ,
"lastKnownPosition" : { "lon" : 0 , "lat" : 0} ",
"userName" : "test123"
}
I cannot think of a single case where a distance would not be returned. So you must be doing something different to how this is represented in this sample:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var geoSchema = new Schema({
"lastKnownPosition": {
"lon": Number,
"lat": Number
},
"userName": String
});
geoSchema.index({ "lastKnownPosition": "2d" });
var Geo = mongoose.model( "Geo", geoSchema, "testgeo" );
mongoose.connection.on("open", function(err,conn) {
async.series(
[
function(callback) {
Geo.remove({},function(err) {
callback();
});
},
function(callback) {
Geo.create(
{
"lastKnownPosition": { "lon": 0, "lat": 0 },
"userName": "test123"
},
function(err,doc) {
if (err) throw err;
callback();
}
);
},
// Mongoose method
function(callback) {
Geo.geoNear(
[0,0],
{
maxDistance: 90,
query: { 'userName': { '$regex': '^test' } }
},
function(err,docs) {
if (err) throw err;
console.log( docs );
callback();
});
},
// Native method
function(callback) {
Geo.db.db.executeDbCommand(
{
"geoNear": "testgeo",
"near": [ 0, 0 ],
"maxDistance": 90,
"query": { 'userName': /^test/ },
},function(err,result) {
if ( err ) throw err;
console.log( result.documents[0].results[0] );
callback();
}
);
},
// aggregate method
function(callback) {
Geo.aggregate(
[
{ "$geoNear": {
"near": [0,0],
"distanceField": "distance",
"maxDistance": 90,
"query": { "userName": { "$regex": "^test" } }
}}
],
function(err,result) {
if (err) throw err;
console.log( result );
callback();
}
);
}
],
function(err) {
mongoose.disconnect();
}
);
});
Which produces output like the following:
[ { dis: 0,
obj:
{ userName: 'test123',
__v: 0,
_id: 54225696ce2837e4495cd188,
lastKnownPosition: { lon: 0, lat: 0 } } } ]
{ dis: 0,
obj:
{ _id: 54225696ce2837e4495cd188,
userName: 'test123',
lastKnownPosition: { lon: 0, lat: 0 },
__v: 0 } }
[ { _id: 54225696ce2837e4495cd188,
userName: 'test123',
lastKnownPosition: { lon: 0, lat: 0 },
__v: 0,
distance: 0 } ]
All have a "distance" field, which is defaulted to "dis" and separate to the document by what is the "geoNear" command in either invocation or is specified and included within the document from the aggregate $geoNear operator.
Follow any of those patterns and you will get the results you want.
You can only create one 2d index per collection, so mongo directly knows which field to query against.
http://docs.mongodb.org/v2.2/core/geospatial-indexes/#create-a-geospatial-index

Categories