Mongoose 'static' methods vs. 'instance' methods - node.js

I believe this question is similar to this one but the terminology is different. From the Mongoose 4 documentation:
We may also define our own custom document instance methods too.
// define a schema
var animalSchema = new Schema({ name: String, type: String });
// assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function (cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
Now all of our animal instances have a findSimilarTypes method available to it.
And then:
Adding static methods to a Model is simple as well. Continuing with our animalSchema:
// assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function (name, cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
}
var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function (err, animals) {
console.log(animals);
});
It seems with static methods each of the animal instances would have the findByName method available to it as well. What are the statics and methods objects in a Schema? What is the difference and why would I use one over the other?

statics are the methods defined on the Model. methods are defined on the document (instance).
You might use a static method like Animal.findByName:
const fido = await Animal.findByName('fido');
// fido => { name: 'fido', type: 'dog' }
And you might use an instance method like fido.findSimilarTypes:
const dogs = await fido.findSimilarTypes();
// dogs => [ {name:'fido',type:'dog} , {name:'sheeba',type:'dog'} ]
But you wouldn't do Animals.findSimilarTypes() because Animals is a model, it has no "type". findSimilarTypes needs a this.type which wouldn't exist in Animals model, only a document instance would contain that property, as defined in the model.
Similarly you wouldn't¹ do fido.findByName because findByName would need to search through all documents and fido is just a document.
¹Well, technically you can, because instance does have access to the collection (this.constructor or this.model('Animal')) but it wouldn't make sense (at least in this case) to have an instance method that doesn't use any properties from the instance. (thanks to #AaronDufour for pointing this out)

Database logic should be encapsulated within the data model. Mongoose provides 2 ways of doing this, methods and statics. Methods adds an instance method to documents whereas Statics adds static “class” methods to the Models itself.The static keyword defines a static method for a model. Static methods aren't called on instances of the model. Instead, they're called on the model itself. These are often utility functions, such as functions to create or clone objects. like example below:
const bookSchema = mongoose.Schema({
title: {
type : String,
required : [true, 'Book name required']
},
publisher : {
type : String,
required : [true, 'Publisher name required']
},
thumbnail : {
type : String
}
type : {
type : String
},
hasAward : {
type : Boolean
}
});
//method
bookSchema.methods.findByType = function (callback) {
return this.model('Book').find({ type: this.type }, callback);
};
// statics
bookSchema.statics.findBooksWithAward = function (callback) {
Book.find({ hasAward: true }, callback);
};
const Book = mongoose.model('Book', bookSchema);
export default Book;
for more info: https://osmangoni.info/posts/separating-methods-schema-statics-mongoose/

Well to me it doesn't mean add anythings by adding Mongoose in front of 'static' or even in front 'instance' keyword.
What I believe meaning and purpose of static is same everywhere, even it's also true for an alien language or some sort of driver which represents Model for building the block like another object-oriented programming. The same also goes for instance.
From Wikipedia:
A method in object-oriented programming (OOP) is a procedure associated with a message and an object. An object consists of data and behavior. The data and behavior comprise an interface, which specifies how the object may be utilized by any of various consumers[1] of the object.
Data is represented as properties of the object and behaviors are represented as methods of the object. For example, a Window object could have methods such as open and close, while its state (whether it is opened or closed at any given point in time) would be a property.
Static methods are meant to be relevant to all the instances of a class rather than to any specific instance. They are similar to static variables in that sense. An example would be a static method to sum the values of all the variables of every instance of a class. For example, if there were a Product class it might have a static method to compute the average price of all products.
Math.max(double a, double b)
This static method has no owning object and does not run on an instance. It receives all information from its arguments.[7]
A static method can be invoked even if no instances of the class exist yet. Static methods are called "static" because they are resolved at compile time based on the class they are called on and not dynamically as in the case with instance methods, which are resolved polymorphically based on the runtime type of the object.
https://en.wikipedia.org/wiki/Method_(computer_programming)

As everybody said, use methods when we want to operate on a single document, and we use statics when we want to operate on entire collection. But why?
Let's say, we have a schema:
var AnimalSchema = new Schema({
name: String,
type: String
});
now as mentioned in the docs, if you need to check the similar types of a particular document in the db
you can do:
AnimalSchema.methods.findSimilarType = function findSimilarType (cb) {
return this.model('Animal').find({ type: this.type }, cb);
};
Now, this here refers to the document itself. So, what that means is, this document
doesn't knows which model it belongs to, because methods has nothing to do with the model defaultly, It's only for that particular document obj.
But we can make it work with the model, using this.model('anyModelName').
Now why did we used methods in the case of finding similar types of animals?
For finding similar types of animals we must have an animal obj for which we'll find similar types of.
That animal obj we have let's say:
const Lion = await new Animal({name: Lion, type: "dangerous"});
Next, when we find similar types we need the Lion obj first, we must have it.
So simply, whenever we need the help of a particular obj/document for doing something, We'll use methods.
Now here by chance we also need whole model to search slimier types,
although it is not available directly in methods (remember this.modelName will return undefined). we can get it by setting this.model() to our preferred model, in this case Animal.
That's all for methods.
Now, statics has the whole model at its disposal.
1)Let's say you want to calculate the total price (assume the model has a price field) of all Animals you'll use statics [for that you don't need any particular Animal obj, so we won't use method]
2)You want to find the animals which have yellow skin (assume the model has a skin field), you'll use statics. [ for that we don't need any particular Animal obj, so we won't use method]
eg:
AnimalSchema.statics.findYellowSkin = function findSimilarType (cb) {
return this.find({ skin: "yellow" }, cb);
};
Remember, In methods this refers to the model so, this.modelName will return Animal (in our case).
but just like methods, here also we can (but we don't need to) set the model using.
AnimalSchema.methods.findSimilarType = function findSimilarType (cb) {
return this.model('Animal').find({ skin: "yellow" }, cb); //just like in methods
};
so as you can see both of statics and methods are very similar.
Whenever you have a document and you need something to do with that,
use methods. Whenever you need to do something with the whole
model/collection, use statics.

Static methods apply to the entire model on which they are defined whereas instance methods apply only to a specific document within the collection.
this in the context of a static method returns the entire model whereas this in the context of an instance method returns the document.
Lets say for example:
const pokemon = new mongoose.Schema({})
pokemon.statics.getAllWithType = function(type){
// Query the entire model and look for pokemon
// with the same type
// this.find({type : type})
}
pokemon.methods.sayName = function(){
// Say the name of a specific pokemon
// console.log(this.name)
}
const pokemonModel = mongoose.model('schema', pokemon)
const squirtle = new pokemonModel({name : "squirtle"})
// Get all water type pokemon from the model [static method]
pokemonModel.getAllWithType("water")
// Make squirtle say its name [instance method]
squirtle.sayName()

Use .statics for static methods.
Use .methods for instance methods.
//instance method
bookSchema.methods.findByType = function (callback) {
return this.model('Book').find({ type: this.type }, callback);
};
// static method
bookSchema.statics.findBooksWithAward = function (callback) {
Book.find({ hasAward: true }, callback);
};

statics are pretty much the same as methods but allow for defining functions that exist directly on your Model.
statics belongs to the Model and methods belongs to an Instance

Related

What is a Mongoose model property that contains 'ref' but does not specify type?

Very new to Mongoose -- I'm working on an existing project and have been given the task of changing some model properties. I understand that if a model contains a property of this type
postedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
this property references another model/schema, and to get access to that linked model/schema one needs to populate it to gain access to this property.
But in the code I'm reviewing (which I didn't write) there are many properties of this type
contentTypes: [{ ref: 'ContentType' }],
source: { ref: 'Source',required: true },
where another schema is referenced, but there is no type. Is this the same sort of relationship, and the id is implied? Is this a subdocument?
As an additional question: if in a model I wanted to refer to a property of a linked model (or schema), would I need to populate first? That is, if it's a subdocument, I can just use dot notation, but if it is a "linked" document, I'm not sure.
The answer was that the model schemas do not stand on their own, but are passed to a model "factory", which gives them the property types they need.
Thus from that factory the following snippet (below). I looked into the documentation for mongoose-autopopulateand I don't see what autopopulate=truemeans.
new: function(name, properties, statics, methods, schemaMods) {
// Add default definition to properties with references and load reference schemas
Object.keys(properties).forEach(function(key) {
var modifiedProperty = (property) => {
if (property.ref) {
property.autopopulate = true;
property.type = mongoose.Schema.Types.ObjectId;
}
return property;
};
if (Array.isArray(properties[key]) && properties[key].length === 1) {
properties[key][0] = modifiedProperty(properties[key][0]);
} else {
properties[key] = modifiedProperty(properties[key]);
}
});

Sequelize: setter for association does not update simple member for it

I have Students and Classes with a hasMany association between them.
I do this:
myStudent.setClasses(ids). then(function(result) {
console.log(myStudent.Classes);
});
Questions:
What does result parameter mean inside the then-handler?
Why isn't myStudent.Classes up-to-date with the setClasses() change I made?
How can I have Sequelize update the simple Classes member? I need to return a simple JSON response to the caller.
According to docs, result would be the associated Classes (in you case) when sending them to the .setClasses method.
Therefore, your ids param should be in fact the Classes, perhaps you should require them before
Class.findAll({where: {id: ids}})
.on('success', function (classes) {
myStudent.setClasses(classes)
.on('success', function (newAssociations) {
// here now you will have the new classes you introduced into myStudent
// you say you need to return a JSON response, maybe you could send this new associations
})
})
It's not updating because the queries regarding the associations of objects doesn't rely on you original object (myStudent). You should add the new associations (result var, in your example, newAssociations, in mine) in your existing myStudent.Classes array. Maybe reloading your instance should work as well.
Class.findAll({where: {id: ids}})
.on('success', function (classes) {
myStudent.setClasses(classes)
.on('success', function (newAssociations) {
myStudent.Classes = myStudent.Classes || [];
myStudent.Classes.push(newAssociations);
// guessing you're not using that myStudent obj anymore
res.send(myStudent);
})
})
I hope I answered this one with the previous two answers, if not, could you explain what you mean by updating the Classes member?

How to get the model name or model class of a waterline record?

While different from a previous question I asked here, it's related and so wanted to link it.
I have been trying hard to find out how I can get the model name (identity) or model "class" (exposed in sails.models) of a record. So, given a waterline record, how can I find out its model name or class?
Example (of course here I know the model is User but that is an example):
User.findOne(1).exec(function(err, record) {
// at this point think that we don't know it's a `user` record
// we just know it's some record of any kind
// and I want to make some helper so that:
getTheModelSomehow(record);
// which would return either a string 'user' or the `User` pseudo-class object
});
I have tried to access it with record.constructor but that is not User, and I couldn't find any property on record exposing either the model's pseudo-class object or the record's model name.
UPDATE:
To clarify, I want a function to which I'll give ANY record, and which would return the model of that record as either the model name or the model pseudo-class object as in sails.models namespace.
modelForRecord(record) // => 'user' (or whatever string being the name of the record's model)
or
modelForRecord(record) // => User (or whatever record's model)
WOW, ok after hours of research, here is how I am doing it for those who are interested (it's a very tricky hack, but for now unable to find another way of doing):
Let's say record is what you get from a findOne, create, ... in the callback, to find out what instance it is, and so find the name of the model owning the record, you have to loop over all your models (sails.models.*) and make an instanceof call this way:
function modelFor(record) {
var model;
for (var key in sails.models) {
model = sails.models[key];
if ( record instanceof model._model.__bindData__[0] ) {
break;
}
model = undefined;
}
return model;
}
Do not try to simply do instanceof model, that does not work
After if you need the model name simply modelFor(record).globalId to get it.
In your model definition why not just create a model attribute. Then the model will be returned with every record call. This will work even after the record become a JSON object.
module.exports = {
attributes : {
model : {type:'string',default:'User'}
}
}
Use this example for parsing model name
var parseModel = function(request) {
request = request.toLowerCase();
return sails.models[request];
};
and use in controller this code for using this model
parseModel(req.param('modelname')).find().then().catch();
Sails exposes everything in the request object. Try grabbing the name of the model this way:
var model = req.options.model || req.options.controller;
That will give you the raw name. To use it, you'll have to plug the model into the sails models array.
var Model = req._sails.models[model];
Check out the source code to see it in action. (https://github.com/balderdashy/sails/blob/master/lib/hooks/blueprints/actionUtil.js#L259)

What is the difference between methods and statics in Mongoose?

What is the difference between methods and statics?
Mongoose API defines statics as
Statics are pretty much the same as methods but allow for defining functions that exist directly on your Model.
What exactly does it mean? What does existing directly on models mean?
Statics code example from documentation:
AnimalSchema.statics.search = function search (name, cb) {
return this.where('name', new RegExp(name, 'i')).exec(cb);
}
Animal.search('Rover', function (err) {
if (err) ...
})
Think of a static like an "override" of an "existing" method. So pretty much directly from searchable documentation:
AnimalSchema.statics.search = function search (name, cb) {
return this.where('name', new RegExp(name, 'i')).exec(cb);
}
Animal.search('Rover', function (err) {
if (err) ...
})
And this basically puts a different signature on a "global" method, but is only applied when called for this particular model.
Hope that clears things up a bit more.
It seems like
'method' adds an instance method to documents constructed from Models
whereas
'static' adds static "class" methods to the Model itself
From the documentation:
Schema#method(method, [fn])
Adds an instance method to documents constructed from Models compiled from this schema.
var schema = kittySchema = new Schema(..);
schema.method('meow', function () {
console.log('meeeeeoooooooooooow');
})
Schema#static(name, fn)
Adds static "class" methods to Models compiled from this schema.
var schema = new Schema(..);
schema.static('findByName', function (name, callback) {
return this.find({ name: name }, callback);
});

Mongoose: what's up with "_doc"?

It seems Mongoose is doing something really funky internally.
var Foo = new mongoose.model('Foo', new mongoose.Schema({a: String, b: Number}));
var foo = new Foo({a: 'test'; b: 42});
var obj = {c: 1};
foo.goo = obj; // simple object assignment. obj should be
// passed by reference to foo.goo. recall goo
// is not defined in the Foo model schema
console.log(foo.goo === obj); // comparison directly after the assignment
// => false, doesn't behave like normal JS object
Essentially, any time you try to deal with properties of a Mongoose model that aren't
a) defined in the model's schema or
b) defined as the same type (array, obj, ..) ... the model doesn't even behave like a normal Javascript object.
Switching line 4 to foo._doc.goo = obj makes the console output true.
edit: trying to reproduce weirdness
example 1:
// Customer has a property 'name', but no property 'text'
// I do this because I need to transform my data slightly before sending it
// to client.
models.Customer.find({}, function(err, data) {
for (var i=0, len=data.length; i<len; ++i) {
data[i] = data[i]._doc; // if I don't do this, returned data
// has no 'text' property
data[i].text = data[i].name;
}
res.json({success: err, response:data});
});
_doc exist on the mongoose object.
Because mongooseModel.findOne returns the model itself, the model has structure (protected fields).
When you try to print the object with console.log it gives you only the data from the database, because console.log will print the object public fields.
If you try something like JSON.stringify then you get to see inside the mongoose model object. (_doc, state ...)
In the case where you want to add more fields in the object and it's not working
const car = model.findOne({_id:'1'})
car.someNewProp = true // this will not work
If later you set the property to the object car and you didn't specify in the Model Schema before then Mongoose model is validating if this field exists and if it's the valid type.
If the validation fails then the property will not be set.
Update
Maybe I misunderstood your original question, but now it looks like the nature of your question changed, so the below information isn't relevant, but I'm leaving it. :)
I tested your code and it works fine for me. Mongoose doesn't execute any special code when you set properties that aren't part of the schema (or a few other special properties). JavaScript currently doesn't support calling code for properties that don't yet exist (so Mongoose can't get in the way of the set of the goo property for example).
So, when you set the property:
foo.goo = { c: 1 };
Mongoose isn't involved. If your console.log was something other than the code you displayed, I could see that it might report incorrectly.
Additionally, when you send the results back as JSON, JSON.stringify is being called, which calls toString on your Mongoose Model. When that happens, Mongoose only uses the properties defined on the schema. So, no additional properties are being sent back by default. You've changed the nature of the data array though to directly point at the Mongoose data, so it avoids that problem.
Details about normal behavior
When you set the property goo using Mongoose, quite a few things happen. Mongoose creates property getters/setters via the Object.defineProperty (some docs). So, when you set the goo property, which you've defined as a [String], a few things happen:
Mongoose code is called prior to the value being set onto the object instance (unlike a simple JavaScript object)
Mongoose creates an array (optionally) to store the data (a MongooseArray) which will contain the array data. In the example you provided, since you didn't pass an array, it will be created.
Mongoose will attempt to cast your data to the right type
It will call toString on the data passed as part of the cast.
So, the results are that the document now contains an array with a toString version of the object you passed.
If you checked the contents of the goo property, you'd see that it's now an array with a single element, which is a string that contains [object Object]. If you'd picked a more basic type or matched the destination property storage type, you would see that a basic equality check would have worked.
you can use toJSON() instead of _doc
Try using lean
By default, Mongoose queries return an instance of the Mongoose Document class. Documents are much heavier than vanilla JavaScript objects, because they have a lot of internal state for change tracking. Enabling the lean option tells Mongoose to skip instantiating a full Mongoose document and just give you the POJO.
https://mongoosejs.com/docs/tutorials/lean.html
Had same problem. Instead of updating my model.
const car = model.findOne({_id:'1'})
let temp = JSON.stringify(car);
let objCar = JSON.parse(temp);
objCar.color = 'Red'; //now add any property you want
this solves my problem
I was stuck on this today... Drove me nuts. Not sure if the below is a good solution (and OP has mentioned it too), but this is how I overcame this issue.
My car object:
cars = [{"make" : "Toyota"}, {"make" : "Kia"}];
Action:
console.log("1. Cars before the color: " + car);
cars.forEach(function(car){
car.colour = "Black"; //color is NOT defined in the model.
});
console.log("2. Cars after the color: " + car);
Problematic console output:
1. Cars before the color: [{"make" : "Toyota"}, {"make" : "Kia"}];
2. Cars after the color: [{"make" : "Toyota"}, {"make" : "Kia"}]; //No change! No new colour properties :(
If you try to pass in this property that was undefined in the model, via doc (e.g. car._doc.color = "black"), it will work (this colour property will be assigned to each car), but you can't seem to access it via EJS (frontend) for some reason.
Solution:
(Again, not sure if this is the best way... but it worked for me): Add in this new property (colour) in the car model.
var carSchema = mongoose.Schema({
make: String,
color: String //New property.
})
With the model redefined, everything worked as normal / expected (no _doc 'hacks' needed etc.) and I lived another day; hope it helps someone else.
There is some weirdness with Mongoose models and you have to check that Mongoose doesn't already have a model created in it's models array.
Here is my solution:
import mongoose from 'mongoose';
createModel = (modelName="foo", schemaDef, schemaOptions = {})=> {
const { Schema } = mongoose;
const schema = Schema(schemaDef, schemaOptions);
const Model = mongoose.models[modelName] || mongoose.model(modelName, schema);
return Model;
}
I use my own mongoose model class and base class for my models. I made this and it should work for you.
For those using spread(...) and/ can't see a solution, here's an example of #entesar's answer
Instead of spread or ._doc in:
import User from "./models/user";
...
async function createUser(req, res) {
const user = await User.create(req.body);
res.status(201).json({
message: "user created",
data: {
...user // OR user._doc,
token: "xxxxxxxx",
},
});
}
...
Use this
import User from "./models/user";
...
async function createUser(req, res) {
const user = await User.create(req.body);
res.status(201).json({
message: "user created",
data: {
...user.toJSON(),
token: "xxxxxxxx",
},
});
}
...
Ps: took me a while to understand the answer.
You should add .lean() on the find to have it skip all the Model "magic".

Resources