Overview

Note:

This relation best works with databases that support foreign keyconstraints (SQL).Using this relation with NoSQL databases will result in unexpected behavior,such as the ability to create a relation with a model that does not exist. We are working on a solution to better handle this. It is fine to use this relation with NoSQL databases for purposes such as navigating related models, where the referential integrity is not critical.

A hasMany relation denotes a one-to-many connection of a model to anothermodel through referential integrity. The referential integrity is enforced by aforeign key constraint on the target model which usually references a primarykey on the source model. This relation indicates that each instance of thedeclaring or source model has zero or more instances of the target model. Forexample, in an application with customers and orders, a customer can have manyorders as illustrated in the diagram below.

hasMany relation illustration

The diagram shows target model Order has property customerId as theforeign key to reference the declaring model Customer’s primary key id.

To add a hasMany relation to your LoopBack application and expose its relatedroutes, you need to perform the following steps:

  • Add a property to your model to access related model instances.
  • Add a foreign key property in the target model referring to the sourcemodel’s id.
  • Modify the source model repository class to provide access to a constrainedtarget model repository.
  • Call the constrained target model repository CRUD APIs in your controllermethods.

Defining a hasMany Relation

This section describes how to define a hasMany relation at the model levelusing the @hasMany decorator. The relation constrains the target repository bythe foreign key property on its associated model. The following example showshow to define a hasMany relation on a source model Customer.

/src/models/customer.model.ts

  1. import {Order} from './order.model';
  2. import {Entity, property, hasMany} from '@loopback/repository';
  3. export class Customer extends Entity {
  4. @property({
  5. type: 'number',
  6. id: true,
  7. })
  8. id: number;
  9. @property({
  10. type: 'string',
  11. required: true,
  12. })
  13. name: string;
  14. @hasMany(() => Order)
  15. orders?: Order[];
  16. constructor(data: Partial<Customer>) {
  17. super(data);
  18. }
  19. }

The definition of the hasMany relation is inferred by using the @hasManydecorator. The decorator takes in a function resolving the target model classconstructor and optionally a custom foreign key to store the relation metadata.The decorator logic also designates the relation type and tries to infer theforeign key on the target model (keyTo in the relation metadata) to a defaultvalue (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 isinferred properly as an array of the target model instances.

The decorated property name is used as the relation name and stored as part ofthe source model definition’s relation metadata. The property type metadata isalso 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 isas follows:

  1. // import statements
  2. class Customer extends Entity {
  3. // constructor, properties, etc.
  4. @hasMany(() => Order, {keyTo: 'customerId'})
  5. orders?: Order[];
  6. }

Add the source model’s id as the foreign key property (customerId) in thetarget model.

/src/models/order.model.ts

  1. import {Entity, model, property} from '@loopback/repository';
  2. @model()
  3. export class Order extends Entity {
  4. @property({
  5. type: 'number',
  6. id: true,
  7. required: true,
  8. })
  9. id: number;
  10. @property({
  11. type: 'string',
  12. required: true,
  13. })
  14. name: string;
  15. @property({
  16. type: 'number',
  17. })
  18. customerId?: number;
  19. constructor(data?: Partial<Order>) {
  20. super(data);
  21. }
  22. }
  23. export interface OrderRelations {
  24. // describe navigational properties here
  25. }
  26. export type OrderWithRelations = Order & OrderRelations;

The foreign key property (customerId) in the target model can be added via acorresponding belongsTo relation, too.

/src/models/order.model.ts

  1. import {Entity, model, property, belongsTo} from '@loopback/repository';
  2. import {Customer, CustomerWithRelations} from './customer.model';
  3. @model()
  4. export class Order extends Entity {
  5. @property({
  6. type: 'number',
  7. id: true,
  8. required: true,
  9. })
  10. id: number;
  11. @property({
  12. type: 'string',
  13. required: true,
  14. })
  15. name: string;
  16. @belongsTo(() => Customer)
  17. customerId: number;
  18. constructor(data?: Partial<Order>) {
  19. super(data);
  20. }
  21. }
  22. export interface OrderRelations {
  23. customer?: CustomerWithRelations;
  24. }
  25. export type OrderWithRelations = Order & OrderRelations;

Configuring a hasMany relation

The configuration and resolution of a hasMany relation takes place at therepository level. Once hasMany relation is defined on the source model, thenthere are a couple of steps involved to configure it and use it. On the sourcerepository, the following are required:

  • In the constructor of your source repository class, useDependency Injection to receive a getter functionfor obtaining an instance of the target repository. Note: We need a getterfunction, accepting a string repository name instead of a repositoryconstructor, or a repository instance, in order to break a cyclic dependencybetween a repository with a hasMany relation and a repository with thematching belongsTo relation.

  • Declare a property with the factory function typeHasManyRepositoryFactory<targetModel, typeof sourceModel.prototype.id> onthe source repository class.

  • call the createHasManyRepositoryFactoryFor function in the constructor ofthe source repository class with the relation name (decorated relationproperty on the source model) and target repository instance and assign it theproperty mentioned above.The following code snippet shows how it would look like:

/src/repositories/customer.repository.ts

  1. import {Order, Customer, CustomerRelations} from '../models';
  2. import {OrderRepository} from './order.repository';
  3. import {
  4. DefaultCrudRepository,
  5. juggler,
  6. HasManyRepositoryFactory,
  7. repository,
  8. } from '@loopback/repository';
  9. import {inject, Getter} from '@loopback/core';
  10. export class CustomerRepository extends DefaultCrudRepository<
  11. Customer,
  12. typeof Customer.prototype.id,
  13. CustomerRelations
  14. > {
  15. public readonly orders: HasManyRepositoryFactory<
  16. Order,
  17. typeof Customer.prototype.id
  18. >;
  19. constructor(
  20. @inject('datasources.db') protected db: juggler.DataSource,
  21. @repository.getter('OrderRepository')
  22. getOrderRepository: Getter<OrderRepository>,
  23. ) {
  24. super(Customer, db);
  25. this.orders = this.createHasManyRepositoryFactoryFor(
  26. 'orders',
  27. getOrderRepository,
  28. );
  29. }
  30. }

The following CRUD APIs are now available in the constrained target repositoryfactory orders for instances of customerRepository:

  • create for creating a target model instance belonging to customer modelinstance(API Docs)
  • find finding target model instance(s) belonging to customer model instance(API Docs)
  • delete for deleting target model instance(s) belonging to customer modelinstance(API Docs)
  • patch for patching target model instance(s) belonging to customer modelinstance(API Docs)For updating (full replace of all properties on a PUT endpoint forinstance) a target model you have to directly use this model repository. In thiscase, the caller must provide both the foreignKey value and the primary key(id). Since the caller already has access to the primary key of the targetmodel, there is no need to go through the relation repository and the operationcan be performed directly on DefaultCrudRepository for the target model(OrderRepository in our example).

Using hasMany constrained repository in a controller

The same pattern used for ordinary repositories to expose their CRUD APIs viacontroller methods is employed for hasMany repositories. Once the hasManyrelation has been defined and configured, controller methods can call theunderlying constrained repository CRUD APIs and expose them as routes oncedecorated withRoute decorators. Itwill require the value of the foreign key and, depending on the request method,a value for the target model instance as demonstrated below.

src/controllers/customer-orders.controller.ts

  1. import {post, param, requestBody} from '@loopback/rest';
  2. import {CustomerRepository} from '../repositories/';
  3. import {Customer, Order} from '../models/';
  4. import {repository} from '@loopback/repository';
  5. export class CustomerOrdersController {
  6. constructor(
  7. @repository(CustomerRepository)
  8. protected customerRepository: CustomerRepository,
  9. ) {}
  10. @post('/customers/{id}/order')
  11. async createOrder(
  12. @param.path.number('id') customerId: typeof Customer.prototype.id,
  13. @requestBody() orderData: Order,
  14. ): Promise<Order> {
  15. return this.customerRepository.orders(customerId).create(orderData);
  16. }
  17. }

In LoopBack 3, the REST APIs for relations were exposed using static methodswith the name following the pattern {methodName}{relationName} (e.g.Customer.find__orders). We recommend to create a new controller for eachrelation in LoopBack 4. First, it keeps controller classes smaller. Second, itcreates a logical separation of ordinary repositories and relationalrepositories and thus the controllers which use them. Therefore, as shown above,don’t add order-related methods to CustomerController, but instead create anew CustomerOrdersController class for them.

Note:

The type of orderData above will possibly change to Partial<Order> to excludecertain properties from the JSON/OpenAPI spec schema built for the requestBodypayload. See its GitHubissue to follow the discussion.