From Fabien: Normally, properties
are transferred from parent to child, but there are cases
where it makes sense to do the opposite. This was the case with the advanced example, and to enable this, a new option: invertProperties
was introduced.
What's best place for this?
Overview of model relations
Individual models are easy to understand and work with. But in reality, models are often connected or related. When you build a real-world application with multiple models, you'll typically need to define relations between models. For example:
- A customer has many orders and each order is owned by a customer.
- A user can be assigned to one or more roles and a role can have zero or more users.
- A physician takes care of many patients through appointments. A patient can see many physicians too.
With connected models, LoopBack exposes as a set of APIs to interact with each of the model instances and query and filter the information based on the client’s needs.
You can define the following relations between models:
- BelongsTo relations
- HasMany relations
- HasManyThrough relations
- HasAndBelongsToMany relations
- Polymorphic relations
- Embedded relations (embedsOne and embedsMany)
You can define models relations in JSON in the Model definition JSON file file or in JavaScript code. The end result is the same.
When you define a relation for a model, LoopBack adds a set of methods to the model, as detailed in the article on each type of relation.
Relation options
There are three options for most relation types:
- Scope
- Properties
- Custom scope methods
"Most"? Which ones DON'T they apply to?
Scope
The scope property can be an object or function, and applies to all filtering/conditions on the related scope.
The object or returned object (in case of the function call) can have all the usual filter options: where
, order
, include
, limit
, offset
, ...
These options are merged into the default filter, which means that the where
part will be AND
-ed. The other options usually override the defaults (standard mergeQuery behavior).
When scope is a function, it will receive the current instance, as well as the default filter object.
For example:
// only allow products of type: 'shoe', always include products Category.hasMany(Product, { as: 'shoes', scope: { where: { type: 'shoe' }, include: 'products' }); Product.hasMany(Image, { scope: function(inst, filter) { return { type: inst.type }; } }); // inst is a category - match category type with product type.
Properties
You can specify the properties
option in two ways:
- As an object: the keys refer to the instance, the value will be the attribute key on the related model (mapping)
- As a function: the resulting object (key/values) are merged into the related model directly.
For example, the following relation transfers the type to the product, and de-normalizes the category name into categoryName
on creation:
Category.hasMany(Product, { as: 'shoes', properties: { type: 'type', category: 'categoryName' });
To accomplish the same thing with a callback function:
Product.hasMany(Image, { properties: function(inst) { // inst is a category return { type: inst.type, categoryName: inst.name }; } });
Custom scope methods
Finally, you can add custom scope methods using the scopeMethods
property. Again, the option can be either an object
or a function
(advanced use).
For example:
var reorderFn = function(ids, cb) { // `this` refers to the RelationDefinition console.log(this.name); // `images` (relation name) // do some reordering here & save cb(null, [3, 2, 1]); }; // manually declare remoting params reorderFn.shared = true; reorderFn.accepts = { arg: 'ids', type: 'array', http: { source: 'body' } }; reorderFn.returns = { arg: 'ids', type: 'array', root: true }; reorderFn.http = { verb: 'put', path: '/images/reorder' }; Product.hasMany(Image, { scopeMethods: { reorder: reorderFn } });
Exposing REST APIs for related models
The following example demonstrates how to access connected models via REST APIs.
var db = loopback.createDataSource({connector: 'memory'}); Customer = db.createModel('customer', { name: String, age: Number }); Review = db.createModel('review', { product: String, star: Number }); Order = db.createModel('order', { description: String, total: Number }); Customer.scope("youngFolks", {where: {age: {lte: 22}}}); Review.belongsTo(Customer, {foreignKey: 'authorId', as: 'author'}); Customer.hasMany(Review, {foreignKey: 'authorId', as: 'reviews'}); Customer.hasMany(Order, {foreignKey: 'customerId', as: 'orders'}); Order.belongsTo(Customer, {foreignKey: 'customerId'});
The code is available at https://github.com/strongloop/loopback-example-relations-basic.
If you run the example application, the REST API is available at http://localhost:3000/api. The home page at http://0.0.0.0:3000/ contains the links shown below. Here are the example endpoints and queries:
Endpoint | Description |
---|---|
/api/customers | List all customers |
/api/customers?filter[fields][0]=name | List all customers with the name property only |
/api/customers/1 | Return customer record for id of 1 |
/api/customers/youngFolks | List a predefined scope ‘youngFolks’ |
/api/customers/1/reviews | List all reviews posted by a given customer |
/api/customers/1/orders | List all orders placed by a given customer |
/api/customers?filter[include]=reviews | List all customers including their reviews |
/api/customers?filter[include][reviews]=author | List all customers including their reviews which also includes the author |
/api/customers?filter[include][reviews]=author&filter[where][age]=21 | List all customers whose age is 21, including their reviews which also includes the author |
/api/customers?filter[include][reviews]=author&filter[limit]=2 | List first two customers including their reviews which also includes the author |
/api/customers?filter[include]=reviews&filter[include]=orders | List all customers including their reviews and orders |