Weird mongoose behavior when viewing ObjectIds? - node.js

When viewing a sub-document with Robomongo I see something like this:
"views" : [
ObjectId("53a478431275cf0f3d91e27d"),
ObjectId("53a478431275cf0f3d91e27d")
]
But when I pull down the object through Mongoose into node.js, I see something like this:
views:
[ { _bsontype: 'ObjectID',
id: 'T\u001aôj#Ü«m¢©Ö',
viewDate: '2015-07-07T23:21:32.259Z' } ]
Yes, the schema is a little different, and I'm trying to write a script to remediate the data into the new format.
The schema is currently
views: [{view:{type: Schema.Types.ObjectId, ref: 'users'},viewDate:{type: Date, default: Date.now}}],
But
A) Why does the view object look all messed up in the latter, and
B) How can I get what I see in Robomongo? (Answered. See edit)
EDIT: Question B is answered. If I do .lean() to my query, then I'll be able to get it back as a non-mongoose object and it'll look how I expect it to look. So that just leaves question A

I managed to reproduce this.
First, you declared a schema similar to this:
views : { type : Schema.Types.ObjectId, ref : 'users' }
You created and wrote documents to the database using that schema.
Then you changed the schema to your current:
views: [{
view : { type: Schema.Types.ObjectId, ref: 'users' },
viewDate : { type: Date, default: Date.now }
}]
Using that schema, you are reading the documents that you wrote to the database using the first schema.
Those schema are fundamentally different: the first is stored as a single ObjectId in the database (the term "subdocument" is a bit confusing, because in Mongoose, subdocuments are documents that are stored with their parent document; the method you're using is called "population" in Mongoose-speak), but the second schema makes views an array of documents that have two properties (view, which is stored as an ObjectId and viewData which is a date).
This confuses Mongoose because it tries to apply the second schema to documents that were written using the first schema, and because of that, it's showing the internal representation of an ObjectId object instead of a stringified version of it.
This also explains why .lean() shows the correct results, because that tells Mongoose to return raw documents (as they are stored in the database) instead of trying to convert them according to the schema.

Related

Non-existing field in Mongodb document appears in mongoose findById() result

I'm somewhat new in what is related to Mongoose and I came to this behaviour I consider as strange. The document returned by Mongoose has fields that are not present in the actual MongoDb document, and seem to be added by Mongoose based on the schema.
I use a schema similar to this (this one is simplified) :
const ProfessionalSchema = new mongoose.Schema({
product: {
details: [{
_id: false,
id: String, // UUID
name: String,
prestations: [{
_id: false,
id: String, // UUID
name: String,
price: Number,
}],
}],
},
[...]
My document as shown in Mongodb with mongo CLI utility doesn't have a product field.
What I don't understand is why the result of Professional.findById().exec() returns a document with a product:{details[]} field. I expect not to have that field in the Mongoose returned result, since it is not present in the original MongoDb document.
The Mongoose documentation found https://mongoosejs.com/docs/guide.html (Schema and Model paragraph) didn't help.
My business logic would require that field not to be present, instead of being forced by the schema. Is this achievable ?
Try taking a look at the default option. You could e.g. default your product to null and then, in your business logic, handle the "product is null" case rather than the "product field does not exist" case.
As for why this is happening, it's because you're dealing with a schema. If the field doesn't exist on the document, it's going to be auto-populated. The whole point of a schema is to ensure consistency of your document structure.

Mongoose : How to find documents in which fields match an ObjectId or a string?

This is mongoose DataModel in NodeJs
product: {type: mongoose.Schema.Types.ObjectId, ref: 'products', required: true}
But in DB, this field is having multiple type of values in documents, have String and ObjectId
I'm querying this in mongoose
{
$or: [
{
"product": "55c21eced3f8bf3f54a760cf"
}
,
{
"product": mongoose.Types.ObjectId("55c21eced3f8bf3f54a760cf")
}
]
}
But this is only fetching the documents which have that field stored as ObjectId.
Is there any way that it can fetch all the documents having both type of values either String OR ObjectId?
Help is much appreciated. Thanks
There is a schema in Mongoose, so when you query a document, it will search it by this schema type. If you change the model's product type to "string", it will fetch only documents with string IDs.
Even if there is a way to fetch either a string OR ObjectId, it's smelly to me to have such inconsistency.
I've encountered the same problem, so the solution was to standardize all documents by running a script to update them.
db.products.find().forEach(function(product) {
db.products.update({ type: product.type},{
$set:{ type: ObjectId(data.type)}
});
});
The only problem I see there is if this type field is actually an _id field. _id fields in MongoDB are immutable and they can't be updated. If that is your case, you can simply create a new document with the same (but parsed) id and remove the old one.
This is what I did: (in Robomongo)
db.getCollection('products').find().forEach(
function(doc){
var newDoc = doc;
newDoc._id = ObjectId(doc._id);
db.getCollection('products').insert(newDoc);
}
)
Then delete all documents, which id is a string:
db.getCollection('products').find().forEach(
function(doc){
db.getCollection('products').remove({_id: {$regex: ""}})
}
)
There is another way to do this. If we Update the type to Mixed then it will fetch all the documents with each type, either String or ObjectId
Define this in your Schema
mongoose.Schema.Types.Mixed
Like
product: {type: mongoose.Schema.Types.Mixed, required: true}

Ways to remove sub-documents in Mongoose

I am currently trying to design a schema structure with mongoose which looks like the following:
var hubSchema = new mongoose.Schema({
//some other properties
dataStream: {
dataType: String,
dataPoints: [{
createdAt: { type: Date, expires: '7d'}
data: {}
}],
storeStrategy: {
type: String,
enum: storeStrategies
}
},
});
The mongoose API docs say that:
Sub-documents enjoy all the same features as normal documents. The
only difference is that they are not saved individually, they are
saved whenever their top-level parent document is saved.
I want dataPoints to be an array of sub-documents and each sub-document should have TTL set just as a normal document. Having said that, I found out from other posts that it is not possible to set 'expires' for sub-documents. So my question is: Should I create a separate model for dataPoints and store a references here or I should implement some custom strategy for deleting sub-documents keeping this kind of structure?

Mongoose: How to populate 2 level deep population without populating fields of first level? in mongodb

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.

DB-ref in mongoose without Schema.ObjectId?

in my code people can follow other people.
So far everything is ok apart from this fact: in the userScheme I have this field.
, following: [{ type: Schema.ObjectId, ref: 'Users' }]
Since every user has an username, it's more versatile for me to use dbref with the username.
is there a way to do something like this?
, following: [{ type: Users.username, ref: 'Users' }]
Many thanks,
g
No, only ObjectId values that refer to the _id property of another collection can be used as refs.
Confirmed in the source code.
You can only reference via ObjectId to a collection like your first code snippet.
[{ type: Schema.ObjectId, ref: 'Users' }]
When making a query with populate(), you can specify the fields you want to return. In your case, it woud look something like
User.find({}).populate('following', 'username').exec(function(err,doc) {});
Each object in the following array would look like
{ _id: 'xxxxxxxxxx', username: 'xxxxxxxx' }
mongoose's populate function is mongodb's lookup in the abstract but populate only creates refs to object id but if you are sure then you will have a unique username for every user and then if you want to populate data using username then you can use lookup and result will be the same.

Resources