-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docs(repository): describe HasMany relation #1500
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
--- | ||
lang: en | ||
title: 'Relations' | ||
keywords: LoopBack 4.0, LoopBack 4 | ||
tags: | ||
sidebar: lb4_sidebar | ||
permalink: /doc/en/lb4/HasMany-relation.html | ||
summary: | ||
--- | ||
|
||
## Overview | ||
|
||
A `hasMany` relation denotes a one-to-many connection of a model to another | ||
model through referential integrity. The referential integrity is enforced by a | ||
foreign key constraint on the target model which usually references a primary | ||
key on the source model. This relation indicates that each instance of the | ||
declaring or source model has zero or more instances of the target model. For | ||
example, in an application with customers and orders, a customer can have many | ||
orders as illustrated in the diagram below. | ||
|
||
![hasMany relation illustration](./imgs/hasMany-relation-example.png) | ||
|
||
The diagram shows target model **Order** has property **customerId** as the | ||
foreign key to reference the declaring model **Customer's** primary key **id**. | ||
|
||
To add a `hasMany` relation to your LoopBack application and expose its related | ||
routes, you need to perform the following steps: | ||
|
||
1. Add a property to your model to access related model instances. | ||
2. Modify the source model repository class to provide access to a constrained | ||
target model repository. | ||
3. Call the constrained target model repository CRUD APIs in your controller | ||
methods. | ||
|
||
## Defining a hasMany Relation | ||
|
||
This section describes how to define a `hasMany` relation at the model level | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this sentence is redundant :p There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, but I added it to make it clear to the readers what the section is about per #1500 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, how about something like this: I just don't like having description of documentation details in docs. They make me feel like I'm reading abstracts of school reports 😨 |
||
using the `@hasMany` decorator. The relation constrains the target repository by | ||
the foreign key property on its associated model. The following example shows | ||
how to define a `hasMany` relation on a source model `Customer`. | ||
|
||
{% include code-caption.html content="/src/models/customer.model.ts" %} | ||
|
||
```ts | ||
import {Order} from './order.model.ts'; | ||
import {Entity, property, hasmany} from '@loopback/repository'; | ||
|
||
export class Customer extends Entity { | ||
@property({ | ||
type: 'number', | ||
id: true, | ||
}) | ||
id: number; | ||
|
||
@property({ | ||
type: 'string', | ||
required: true, | ||
}) | ||
name: string; | ||
|
||
@hasMany(Order) orders?: Order[]; | ||
|
||
constructor(data: Partial<Customer>) { | ||
super(data); | ||
} | ||
} | ||
``` | ||
|
||
The definition of the `hasMany` relation is inferred by using the `@hasMany` | ||
decorator. The decorator takes in the target model class constructor and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also show what the relation metadata/how it should be done in JS here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to show what the relation metadata would look like, I had that included at first, but removed it later per #1500 (comment). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as the information is captured somewhere. In that case, we should add in some TSdocs for |
||
optionally a custom foreign key to store the relation metadata. The decorator | ||
logic also designates the relation type and tries to infer the foreign key on | ||
the target model (`keyTo` in the relation metadata) to a default value (source | ||
model name appended with `id` in camel case, same as LoopBack 3). It also calls | ||
`property.array()` to ensure that the type of the property is inferred properly | ||
as an array of the target model instances. | ||
|
||
The decorated property name is used as the relation name and stored as part of | ||
the source model definition's relation metadata. The property type metadata is | ||
also preserved as an array of type `Order` as part of the decoration. | ||
|
||
A usage of the decorator with a custom foreign key name for the above example is | ||
as follows: | ||
|
||
```ts | ||
// import statements | ||
class Customer extends Entity { | ||
// constructor, properties, etc. | ||
@hasMany(Order, {keyTo: 'custId'}) | ||
orders?: Order[]; | ||
} | ||
``` | ||
|
||
## Configuring a hasMany relation | ||
|
||
The configuration and resolution of a `hasMany` relation takes place at the | ||
repository level. Once `hasMany` relation is defined on the source model, then | ||
there are a couple of steps involved to configure it and use it. On the source | ||
repository, the following are required: | ||
|
||
- Use [Dependency Injection](Dependency-injection.md) to inject an instance of | ||
the target repository in the constructor of your source repository class. | ||
- Declare a property with the factory function type | ||
`HasManyRepositoryFactory<targetModel, typeof sourceModel.prototype.id>` on | ||
the source repository class. | ||
- call the `_createHasManyRepositoryFactoryFor` function in the constructor of | ||
the source repository class with the relation name (decorated relation | ||
property on the source model) and target repository instance and assign it the | ||
property mentioned above. | ||
|
||
The following code snippet shows how it would look like: | ||
|
||
{% include code-caption.html | ||
content="/src/repositories/customer.repository.ts.ts" %} | ||
|
||
```ts | ||
import {Order, Customer} from '../models'; | ||
import {OrderRepository} from './order.repository.ts'; | ||
import { | ||
DefaultCrudRepository, | ||
juggler, | ||
HasManyRepositoryFactory, | ||
} from '@loopback/repository'; | ||
|
||
class CustomerRepository extends DefaultCrudRepository< | ||
Customer, | ||
typeof Customer.prototype.id | ||
> { | ||
public orders: HasManyRepositoryFactory<typeof Customer.prototype.id, Order>; | ||
constructor( | ||
@inject('datasources.db') protected db: juggler.DataSource, | ||
@repository(OrderRepository) orderRepository: OrderRepository, | ||
) { | ||
super(Customer, db); | ||
this.orders = this._createHasManyRepositoryFactoryFor( | ||
'orders', | ||
orderRepository, | ||
); | ||
} | ||
} | ||
``` | ||
|
||
The following CRUD APIs are now available in the constrained target repository | ||
factory `orders` for instances of `customerRepository`: | ||
|
||
- `create` for creating a target model instance belonging to customer model | ||
instance | ||
([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyRepository.prototype.create)) | ||
- `find` finding target model instance(s) belonging to customer model instance | ||
([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyRepository.prototype.find)) | ||
- `delete` for deleting target model instance(s) belonging to customer model | ||
instance | ||
([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyRepository.prototype.delete)) | ||
- `patch` for patching target model instance(s) belonging to customer model | ||
instance | ||
([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyRepository.prototype.patch)) | ||
|
||
## Using hasMany constrained repository in a controller | ||
|
||
The same pattern used for ordinary repositories to expose their CRUD APIs via | ||
controller methods is employed for `hasMany` repositories. Once the hasMany | ||
relation has been defined and configured, controller methods can call the | ||
underlying constrained repository CRUD APIs and expose them as routes once | ||
decorated with | ||
[Route decorators](Routes.md#using-route-decorators-with-controller-methods). It | ||
will require the value of the foreign key and, depending on the request method, | ||
a value for the target model instance as demonstrated below. | ||
|
||
{% include code-caption.html | ||
content="src/controllers/customer-orders.controller.ts" %} | ||
|
||
```ts | ||
import {post, param, requestBody} from '@loopback/rest'; | ||
import {customerRepository} from '../repositories/'; | ||
import {Customer, Order} from '../models/'; | ||
|
||
export class CustomerOrdersController { | ||
constructor( | ||
@repository(CustomerRepository) | ||
protected customerRepository: CustomerRepository, | ||
) {} | ||
|
||
@post('/customers/{id}/order') | ||
async createOrder( | ||
@param.path.number('id') customerId: typeof Customer.prototype.id, | ||
@requestBody() orderData: Order, | ||
): Promise<Order> { | ||
return await this.customerRepository.orders(customerId).create(orderData); | ||
} | ||
} | ||
``` | ||
|
||
In LoopBack 3, the REST APIs for relations were exposed using static methods | ||
with the name following the pattern `__{methodName}__{relationName}__` (e.g. | ||
`Customer.__find__orders`). We recommend to create a new controller for each | ||
relation in LoopBack 4. First, it keeps controller classes smaller. Second, it | ||
creates a logical separation of ordinary repositories and relational | ||
repositories and thus the controllers which use them. Therefore, as shown above, | ||
don't add order-related methods to `CustomerController`, but instead create a | ||
new `CustomerOrdersController` class for them. | ||
|
||
{% include note.html content=" | ||
The type of `orderData` above will possibly change to `Partial<Order>` to exclude | ||
certain properties from the JSON/OpenAPI spec schema built for the `requestBody` | ||
payload. See its [GitHub | ||
issue](https://github.com/strongloop/loopback-next/issues/1179) to follow the discussion. | ||
" %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
--- | ||
lang: en | ||
title: 'Relations' | ||
keywords: LoopBack 4.0, LoopBack 4 | ||
tags: | ||
sidebar: lb4_sidebar | ||
permalink: /doc/en/lb4/Relations.html | ||
summary: | ||
--- | ||
|
||
## Overview | ||
|
||
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. | ||
|
||
Model relation in LoopBack 3 is one of its powerful features which helps users | ||
define real-world mappings between their models, access sensible CRUD APIs for | ||
each of the models, and add querying and filtering capabilities for the relation | ||
APIs after scaffolding their LoopBack applications. In LoopBack 4, with the | ||
introduction of [repositories](Repositories.md), we aim to simplify the approach | ||
to relations by creating constrained repositories. This means that certain | ||
constraints need to be honoured by the target model repository based on the | ||
relation definition, and thus we produce a constrained version of it as a | ||
navigational property on the source repository. | ||
|
||
Here are the currently supported relations: | ||
|
||
- [HasMany](HasMany-relation.md) | ||
|
||
The articles on each type of relation above will show you how to leverage the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should provide an example of what the controller route of a related model would look like here. I think it'd provide users with good idea of what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well we provide all that information at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, but I think that readers shouldn't be expected to find an example of That being said, probably not a big deal |
||
new relation engine to define and configure relations in your LoopBack | ||
application. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,13 +12,13 @@ import {camelCase} from 'lodash'; | |
// tslint:disable:no-any | ||
|
||
export enum RelationType { | ||
belongsTo, | ||
hasOne, | ||
hasMany, | ||
embedsOne, | ||
embedsMany, | ||
referencesOne, | ||
referencesMany, | ||
belongsTo = 'belongsTo', | ||
hasOne = 'hasOne', | ||
hasMany = 'hasMany', | ||
embedsOne = 'embedsOne', | ||
embedsMany = 'embedsMany', | ||
referencesOne = 'referencesOne', | ||
referencesMany = 'referencesMany', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lovely 👍 |
||
} | ||
|
||
export const RELATIONS_KEY = 'loopback:relations'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please start this documentation page with a short explanation what is
HasMany
relation. I think we can pretty much copy the "Overview" section from https://loopback.io/doc/en/lb3/HasMany-relations.htmlThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
(this has been resolved by content added below the line 10)