MongooseJS - Insert Subdocument without Validation on Document - javascript

I am using Mongoose with Javascript (NodeJS) to read/write to MongoDB. I have a Document (Parent) that has a bunch of Subdocuments (Children) in it. Both my Document and Subdocuments have validation (required: true and a function that validates that the user puts text in the field) defined in their Model.
When attempting to push a new Subdocument into the database, Mongoose rejects my push because validation fails on the Document. This has perplexed me as I am not trying to create a new Document with Subdocument, I am simply trying to push a new Subdocument into an existing Document.
Here is my (example) Mongoose Model:
const mongoose = require('mongoose');
const requiredStringValidator = [
(val) => {
const testVal = val.trim();
return testVal.length > 0;
},
// Custom error text
'Please supply a value for {PATH}',
];
const childrenSchema = new mongoose.Schema({
childId: {
type: mongoose.Schema.Types.ObjectId,
},
firstName: {
type: String,
required: true,
validate: requiredStringValidator,
},
lastName: {
type: String,
required: true,
validate: requiredStringValidator,
},
birthday: {
type: Date,
required: true,
},
});
const parentSchema = new mongoose.Schema(
{
parentId: {
type: mongoose.Schema.Types.ObjectId,
},
firstName: {
type: String,
required: true,
validate: requiredStringValidator,
},
lastName: {
type: String,
required: true,
validate: requiredStringValidator,
},
children: [childrenSchema],
},
{ collection: 'parentsjustdontunderstand' },
);
const mongooseModels = {
Parent: mongoose.model('Parent', parentSchema),
Children: mongoose.model('Children', childrenSchema),
};
module.exports = mongooseModels;
I can successfully push a new Child Subdocument into the Parent Document via the following MongoDB command:
db.parentsjustdontunderstand.update({
firstName: 'Willard'
}, {
$push: {
children: {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968" }
}
});
However, when I follow the Mongoose documentation Adding Subdocs to Arrays and try to add it via Mongoose, it fails.
For testing purposes, I am using Postman and performing a PUT request against an endpoint.
The following is req.body:
{
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
My code is:
const { Parent } = require('parentsModel');
const parent = new Parent();
parent.children.push(req.body);
parent.save();
What I get back is:
ValidationError: Parent validation failed: firstName: Path `firstName` is required...`
and it lists all of the Parent Document's validation requirements.
I could use some help on what I am doing wrong. For the record, I have looked at this answer on Stackoverflow: Push items into mongo array via mongoose but most examples I see do not show or discuss validation in their Mongoose Models.
EDIT 1
Based on feedback from #j-f, I modified my code to below (moving the body out of req.body and just creating it in code for testing purposes. When I attempt to push the update the way recommended, the record gets inserted, however, I still get a validation error thrown to console:
const parent = await Parent.findOne({firstName: 'Willard'});
const child = {
children: {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
}
parent.children.push(child);
parent.save();
ValidationError: Parent validation failed: children.12.firstName: Path `firstName` is required., children.12.lastName: Path `lastName` is required., children.12.birthday: Path `birthday` is required.

ANSWER
#J.F is correct and I am wrong.
This is incorrect:
const child = {
children: {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
}
This is correct:
const child = {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
The record gets inserted into the database and saved but since I was initiating this as a PUT request, I was not properly responding after a successful save with an HTTP 200 OK. Correct code below for the entire solution, however, keep in mind the res.status code is only necessary in this scenario because I was imitating the code via a PUT request.
Mongoose Model:
const mongoose = require('mongoose');
const requiredStringValidator = [
(val) => {
const testVal = val.trim();
return testVal.length > 0;
},
// Custom error text
'Please supply a value for {PATH}',
];
const childrenSchema = new mongoose.Schema({
childId: {
type: mongoose.Schema.Types.ObjectId,
},
firstName: {
type: String,
required: true,
validate: requiredStringValidator,
},
lastName: {
type: String,
required: true,
validate: requiredStringValidator,
},
birthday: {
type: Date,
required: true,
},
});
const parentSchema = new mongoose.Schema(
{
parentId: {
type: mongoose.Schema.Types.ObjectId,
},
firstName: {
type: String,
required: true,
validate: requiredStringValidator,
},
lastName: {
type: String,
required: true,
validate: requiredStringValidator,
},
children: [childrenSchema],
},
{ collection: 'parentsjustdontunderstand' },
);
const mongooseModels = {
Parent: mongoose.model('Parent', parentSchema),
Children: mongoose.model('Children', childrenSchema),
};
module.exports = mongooseModels;
The following is req.body:
{
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
Code is:
const { Parent } = require('parentsModel');
const parent = await Parent.findOne({firstName: 'Willard'});
parent.children.push(req.body);
parent.save((err, doc) => {
if (err) {
res.status(500).json({
message: 'Error finding active projects',
error: err,
});
} else {
res.status(200).json(doc);
}
});

You can push the children into the parent using a mongo query because into the update, the first object is to find the document where do the push.
The syntaxis is like: update({query},{update},{options}). So you are looking for a document with firstName: 'Willard' and adding the children into it.
Here everything is ok, all fields exists, the parent exists into collection so there is no problem.
But using
const parent = new Parent();
parent.children.push(req.body);
parent.save();
Your parent object is empty (unless the constructor fill all fields, but this is not a good idea I think).
If you try this:
var parent = await model.findOne({firstName: 'Willard'})
parent.children.push(req.body);
parent.save();
Then should works.
In this case, the object parent is retrieved from collection, so it contains all necessary fields.
I'm going to edit to explain better why is not thethe two queries are not the same.
Basically the child object you are trying to save has not the same structure as db.collection.update one. Note that the object child you have create to insert into collection has only one property called children. It doesn't have the necessary properties like firstName...
I'm going to use pure JS to can see what is the console.log() output and to see the differences.
Your mongo query push an object like this (translated to js language):
var array = []
array.push(
children = {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
)
console.log(array)
But you are creating the object in this way:
const child = {
children: {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
}
console.log(child)
Do you see now the difference? One object is the children itself the other object has the property children with the necessary fields.
So let's combinate the two pieces of code:
const child = {
children: {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
}
const children = {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
var array = [child,children]
console.log(array)
So, for your code, if you use:
parent.children.push(child.children);
parent.save();
should works. BUT, the best ways is not created the object children inside const child
Try using:
const child = {
"firstName": "Will",
"lastName": "Smith",
"birthday": "9/25/1968"
}
parent.children.push(child);
parent.save();

Related

i have objects in array in mongo db i want to delete all the objects but always left one object with{ id:_oldxxxxxxxx}

I have some code in javascript with moongooes that I used in mongo DB to store a data
Sometimes I need to delete all the objects in array
and get a clean array
this is my schema
const orderSchema =new Schema({
date: {
type: Date,
},
OrderNumber: {
type: String,
required: true
},
City: {
type: String,
required: true
},
Address: {
type: String,
required: true
},
Phone: {
type: String
},
Country: {
type: String
},
Name: {
type: String,
required: true
},
Trackingnumber: {
type: String
},
ZipCode: {
type: Number
},
Province: {
type: String,
},
fulfillmentOrders:{
type: String,
},
Quantity: {
},
});
Holde:[
orderSchema
],
module.exports = mongoose.model('User', userSchema);
and my data on mongo looks like this
"Holde": [
{
"OrderNumber": "gid://shopify/Order/4958122475753",
"City": "xxxx",
"Address": "xxxx",
"Phone": "",
"Country": "xxx",
"Name": "xxx",
"Trackingnumber": "0",
"ZipCode": xxxx,
"fulfillmentOrders": "gid://shopify/FulfillmentOrder/6034089509097",
"Quantity": [
{
"quantity": 1,
"product": {
"id": "gid://shopify/Product/7909915590889"
},
"variant": {
"sku": "11111"
}
}
],
"_id": {
"$oid": "6389b12faaade0788141bf4f"
}
I try to delete all the objects in my array
whit this code
const User = require('../model/User');
const foundUse= await User.findOne({ "user":req.body.user}).exec();
await foundUse.updateOne({
Holde :{
$pull: {'_id':6389b12faaade0788141bf4f},
}
},
)
and expect to get "hold":[]
but actually
I get
"Holde": [
{
"_id": {
"$oid": "6389d882afbc458cc1c1af23"
}
}
],
It's pretty normal because you are updating your User with theses data.
With mongoose, the way to propperly delete item is .deleteMany(), but in your case it will only delete the User (and you seems to want to delete only the order).
You can then filter user's orders and re-assign it without the found order, using FindOneAndUpdate, like:
const User = require('../model/User');
const foundUser = await User.findOne({ "user": req.body.user });
const result = await User.updateOne({
Holde: foundUser?.Holde.filter(holde => holde._id !== HOLD_ID_TO_DELETE)
});
Where HOLD_ID_TO_DELETE is the id to delete (you seems to pass the whole user object with all orders at the moment)
But not it would be prettier and more maintenable to create an Order collection, linked to your User one using ref.

How to push an element into an array inside an object inside another array in Mongoose?

I am developing a server using Expressjs, Mongodb and Mongoose. I need to push an element (a string) into the "tweets" array which is inside an object (a friend) which is in turn inside the "friends" array which is inside a "user" Object which document in the "users" collection. Here is an example of how my documents in the Mongodb collection looks like:
{
"loggedIn": true,
"_id": "5f91ef0ce75d3b1d40539da0",
"username": "username",
"email": "a#h.com",
"password": "$2a$10$9krWS9Kq5024lRTexqaweePrn8aughepqTkaj3oA48x0fJ2ajd79u",
"dateOfBirth": "2002-12-07",
"gender": "male",
"friends": [
{
"tweets": [],
"_id": "5f91effae75d3b1d40539da7",
"username": "Jonas"
},
],
"__v": 0
}
I need to pick the specified username from the "Users" arrary first and then access "friends" array within this user and then pick the right friend object and finally push the tweet on $position: 0 in this array. I I tried to achieve that as shown in this code and I could access the friend object with the given friendUsername
await Users.updateOne(
{ username: req.params.username },
{
$push: {
friends: {
$elemMatch: {
username: req.params.friendUsername,
},
},
},
}
);
And now the question is how to access the "tweets" array inside $elemMatch and push the req.body.tweet at $position: 0 into it?
Here is how I would solve your issue, I first would re-define the way I am defining schemas.
My User schema would look something like the following
User.js
const mongoose = require('mongoose')
const UserSchema = mongoose.Schema({
...
friends: {
type: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
required: true,
default: []
},
tweets: {
type: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Tweet'
}],
required: true,
default: []
},
...
}, {timestamps: true})
module.exports = mongoose.model('User', UserSchema)
User.js
const mongoose = require('mongoose')
const TweetSchema = mongoose.Schema({
...
text: {
type: String,
required: true
},
tweeter: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
likes: {
type: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
required: true,
default: []
},
...
}, {timestamps: true})
module.exports = mongoose.model('Tweet', TweetSchema)
This assumes that every user can have tweets and that a User can be friend with another User
And now if someone tweets something you can do something like
const Tweet = require('./Tweet.js')
const User = require('./User.js')
let tweet = new Tweet({
text: "My first tweet!",
tweeter: "ID Of user who is posting the tweet"
})
tweet.save()
// Now update the user who tweeted
User.findOneAndUpdate()
User.updateOne({ _id: "ID Of user who is posting the tweet" }, { $push: { tweets: tweet._id } })
and now whenever you request a user all of his friends will be referenced and all of their tweets will also be referenced! if you want to see the actual tweets then use something like .populate() here are the docs for .populate() https://mongoosejs.com/docs/populate.html
Keep in mind is really a good practice to only return the actual ids and your frontend takes care of requesting the appropriate objects from their perspective endpoints. And if you wish to reduce network calls then the frontend would cache the data.
If the above doesn't help and you still would like to achieve your goal with your schemas then something like this should work (assuming your schema is called User)
let tweetObj = {}
User.updateOne({_id: 'your userid'}, {$push: {"friends.$.tweets": tweetObj}})
NOTE: I have omitted callbacks as they are irrelevant to the question

Map array of objects by ID and update each existing object in the array

I'm trying to make a call, return an array of objects, then loop through each object in the array by the id and make an additional call. With this second fetch, I need to add a new object to each corresponding object within the original array. Please see code samples below and thank you in advance!
Steps:
Pass search params into postSearchPromise
Map over results and store an array of all id's
Pass each id into the getMediaPromise (I've already defined the token)
Add each response object from the getMediaPromise to the corresponding object in the existing array.
Use a reducer to store the final results (This is a React Native app, i'm using a FlatList on the following screen that points to this stored data, looping through the array and displaying the list on screen)
async function search() {
const toSend = {
title,
age,
location
};
try {
const results = await postSearchPromise(token, toSend);
const theUsers = await results.map(getUsers);
function getUsers(item) {
var users = item.id;
return users;
}
const avatars = await getMediaPromise(token, theUsers);
const mapped = await results.map(element => ({avatar: avatars ,...element}));
dispatch({type: 'SEARCH_RESULTS', payload: mapped});
} catch (e) {
console.log("The Error:" , e);
}
}
Currently, this almost works, but "avatar" takes all of the id's from getUsers and sends them in a comma separated list, together at once, inside the "getMediaPromise," - this is not the intent, but I understand why the code is behaving this way. I'm not sure how to loop through each id and add the new object to each existing object in the array.
The search results I start with:
[
{
id: "123",
name: "John",
location: "USA"
},
{
id: "456",
name: "Jane",
location: "USA"
},
{
id: "789",
name: "Jerry",
location: "USA"
}
]
The search results I need to finish with:
[
{
id: "123",
name: "John",
location: "USA",
avatar: {
type: "default",
status: "200",
ok: true,
url: "http://localhost:3000/media/123"
}
},
{
id: "456",
name: "Jane",
location: "USA",
avatar: {
type: "default",
status: "200",
ok: true,
url: "http://localhost:3000/media/456"
}
},
{
id: "789",
name: "Jerry",
location: "USA",
avatar: {
type: "default",
status: "200",
ok: true,
url: "http://localhost:3000/media/789"
}
}
]
I'm open to an entirely different way to do this, but as mentioned above... I'm using a FlatList on the following screen so it's essential that this is a single array of objects so my FlatList can easily loop over the stored data and pull each piece accordingly. Thanks!

Mongoose: how to query by a key of an object

I have a schema that looks like this
const CustomersSchema = new mongoose.Schema({
merchant:{
type: mongoose.Schema.Types.ObjectId,
required:true,
ref: 'Merchent'
},
idCustomer:{
type: String,
required: true,
trim:true,
//unique: true
},
customerData:{
type:Object,
required:true
}
});
and customerData looks like
{
"name": "Other",
"birthday": null,
"age": null,
"createdAt": "2019-04-01T20:01:04.000Z",
"email": null,
"visits": 0,
"payments": "0.00",
"lastVisit": "2019-12-16T12:58:09.000Z",
}
Is there a way I can query by customerData.name and if it doesn't exist it won't cause errors
Right now I do
let query = {
merchant: merchant._id,
'customerData.name': req.query.name
}
Customer.find(query)
.then(...)
but this way if there is no query.name no documents return
Edit: also regex won't do because I will need to search by numbers(ex: customerData.visits) in the future
It seems like you need to return the document whether or not you have query.name and if there is query.name the query will consider that and if there is no query.name it will not take that into account. So, you can setup a small logic for that. Something like this:
let query = {
merchant: merchant._id
};
// we only set the query for the customerData.name if query.name exist
if(req.query.name) {
query['customerData.name'] = req.query.name;
}
Customer.find(query)
.then(...)

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.

Categories