A model represents a collection of structured data, usually corresponding to a single table or collection in a database. Models are usually defined by creating a file in an app's api/models/
folder.
// Parrot.js
// The set of parrots registered in our app.
module.exports = {
attributes: {
// e.g., "Polly"
name: {
type: 'string'
},
// e.g., 3.26
wingspan: {
type: 'float',
required: true
},
// e.g., "cm"
wingspanUnits: {
type: 'string',
enum: ['cm', 'in', 'm', 'mm'],
defaultsTo: 'cm'
},
// e.g., [{...}, {...}, ...]
knownDialects: {
collection: 'Dialect'
}
}
}
Models may be accessed from our controllers, policies, services, responses, tests, and in custom model methods. There are many built-in methods available on models, the most important of which are the query methods: find, create, update, and destroy. These methods are asynchronous - under the covers, Waterline has to send a query to the database and wait for a response.
Consequently, query methods return a deferred query object. To actually execute a query, .exec(cb)
must be called on this deferred object, where cb
is a callback function to run after the query is complete.
Waterline also includes opt-in support for promises. Instead of calling .exec()
on a query object, we can call .then()
, .spread()
, or .catch()
, which will return a Bluebird promise.
Model class methods are functions built into the model itself that perform a particular task on its instances (records). This is where you will find the familiar CRUD methods for performing database operations like .create()
, .update()
, .destroy()
, .find()
, etc.
Waterline allows you to define custom methods on your models. This feature takes advantage of the fact that Waterline models ignore unrecognized keys, so you do need to be careful about inadvertently overriding built-in methods and dynamic finders (don't define methods named "create", etc.) Custom model methods are most useful for extrapolating controller code that relates to a particular model; i.e. this allows you to pull code out of your controllers and into reusuable functions that can be called from anywhere (i.e. don't depend on req
or res
.)
Model methods are generally asynchronous functions. By convention, asynchronous model methods should be 2-ary functions, which accept an object of inputs as their first argument (usually called opts
or options
) and a Node callback as the second argument. Alternatively, you might opt to return a promise (both strategies work just fine- it's a matter of preference. If you don't have a preference, stick with Node callbacks.)
A best practice is to write your static model method so that it can accept either a record OR its primary key value. For model methods that operate on/from multiple records at once, you should allow an array of records OR an array of primary key values to be passed in. This takes more time to write, but makes your method much more powerful. And since you're doing this to extrapolate commonly-used logic anyway, it's usually worth the extra effort.
For example:
// in api/models/Monkey.js...
// Find monkeys with the same name as the specified person
findWithSameNameAsPerson: function (opts, cb) {
var person = opts.person;
// Before doing anything else, check if a primary key value
// was passed in instead of a record, and if so, lookup which
// person we're even talking about:
(function _lookupPersonIfNecessary(afterLookup){
// (this self-calling function is just for concise-ness)
if (typeof person === 'object') return afterLookup(null, person);
Person.findOne(person).exec(afterLookup);
})(function (err, person){
if (err) return cb(err);
if (!person) {
err = new Error();
err.message = require('util').format('Cannot find monkeys with the same name as the person w/ id=%s because that person does not exist.', person);
err.status = 404;
return cb(err);
}
Monkey.findByName(person.name)
.exec(function (err, monkeys){
if (err) return cb(err);
cb(null, monkeys);
})
});
}
Then you can do:
Monkey.findWithSameNameAsPerson(albus, function (err, monkeys) { ... });
// -or-
Monkey.findWithSameNameAsPerson(37, function (err, monkeys) { ... });
For more tips, read about the incident involving Timothy the Monkey.
Another example:
// api/models/User.js
module.exports = {
attributes: {
name: {
type: 'string'
},
enrolledIn: {
collection: 'Course', via: 'students'
}
},
/**
* Enrolls a user in one or more courses.
* @param {Object} options
* => courses {Array} list of course ids
* => id {Integer} id of the enrolling user
* @param {Function} cb
*/
enroll: function (options, cb) {
User.findOne(options.id).exec(function (err, theUser) {
if (err) return cb(err);
if (!theUser) return cb(new Error('User not found.'));
theUser.enrolledIn.add(options.courses);
theUser.save(cb);
});
}
};
These are special static methods that are dynamically generated by Sails when you lift your app. For instance, if your Person model has a "firstName", you might run:
Person.findByFirstName('emma').exec(function(err,people){ ... });
A special type of model methods which are attached by the pubsub hook. More on that in the section of the docs on resourceful pubsub.
Attribute methods are functions available on records (i.e. model instances) returned from Waterline queries. For example, if you find the ten students with the highest GPA from the Student model, each of those student records will have access to all the built-in attribute methods, as well as any custom attribute methods defined on the Student model.
Every Waterline model includes some attribute methods automatically, including:
Waterline models also allow you to define your own custom attribute methods. Define them like any other attribute, but instead of an attribute definition object, write a function on the right-hand-side.
// From api/models/Person.js...
module.exports = {
attributes: {
// Primitive attributes
firstName: {
type: 'string',
defaultsTo: ''
},
lastName: {
type: 'string',
defaultsTo: ''
},
age: {
type: 'integer'
},
// Associations (aka relational attributes)
spouse: { model: 'Person' },
pets: { collection: 'Pet' },
// Attribute methods
getFullName: function (){
return this.firstName + ' ' + this.lastName;
},
isMarried: function () {
return !!this.spouse;
},
isEligibleForSocialSecurity: function (){
return this.age >= 65;
},
}
};
Note that with the notable exception of the built-in
.save()
and.destroy()
attribute methods, attribute methods are almost always synchronous by convention.Also note that custom attributes methods are not serialized to JSON by default. To serialize them, you can override toJSON.
Custom attribute methods are particularly useful for extracting some information out of a record. I.e. you might want to reduce some information from one or more attributes (i.e. "is this person married?")
if ( rick.isMarried() ) {
// ...
}
You should avoid writing your own asynchronous attribute methods. While built-in asynchronous attribute methods like .save()
and .destroy()
can be convenient from your app code, writing your own asynchronous attribute methods can sometimes have unintended consequences, and is not the most efficient way to build your app.
For instance, consider an app that manages wedding records. You might think to write an attribute method on the Person model that updates the spouse
attribute on both individuals in the database. This would allow you to write controller code like:
personA.marry(personB, function (err) {
if (err) return res.negotiate(err);
return res.ok();
})
Which looks great...until you need to write a different action where you don't have an actual record for "personA".
A better strategy is to write a custom (static) model method instead. This makes your function more reusable/versatile, since it will be accessible whether or not you have an actual record instance on hand. You might refactor the code from the previous example to look like:
Person.marry([joe,raquel], function (err) {
if (err) return res.negotiate(err);
return res.ok();
})
Make sure you use a naming convention that helps you avoid confusing attribute methods from attribute values when you're working with records in your app. A good best practice is to use "get" or "is" (e.g. getFullName()
or isMarried()
) prefix and avoid writing attribute methods that change records in-place.