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.
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
import {Order} from './order.model';
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 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:
// import statements
class Customer extends Entity {
// constructor, properties, etc.
@hasMany(() => Order, {keyTo: 'customerId'})
orders?: Order[];
}
Add the source model’s id as the foreign key property (customerId
) in thetarget model.
/src/models/order.model.ts
import {Entity, model, property} from '@loopback/repository';
@model()
export class Order extends Entity {
@property({
type: 'number',
id: true,
required: true,
})
id: number;
@property({
type: 'string',
required: true,
})
name: string;
@property({
type: 'number',
})
customerId?: number;
constructor(data?: Partial<Order>) {
super(data);
}
}
export interface OrderRelations {
// describe navigational properties here
}
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
import {Entity, model, property, belongsTo} from '@loopback/repository';
import {Customer, CustomerWithRelations} from './customer.model';
@model()
export class Order extends Entity {
@property({
type: 'number',
id: true,
required: true,
})
id: number;
@property({
type: 'string',
required: true,
})
name: string;
@belongsTo(() => Customer)
customerId: number;
constructor(data?: Partial<Order>) {
super(data);
}
}
export interface OrderRelations {
customer?: CustomerWithRelations;
}
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 type
HasManyRepositoryFactory<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
import {Order, Customer, CustomerRelations} from '../models';
import {OrderRepository} from './order.repository';
import {
DefaultCrudRepository,
juggler,
HasManyRepositoryFactory,
repository,
} from '@loopback/repository';
import {inject, Getter} from '@loopback/core';
export class CustomerRepository extends DefaultCrudRepository<
Customer,
typeof Customer.prototype.id,
CustomerRelations
> {
public readonly orders: HasManyRepositoryFactory<
Order,
typeof Customer.prototype.id
>;
constructor(
@inject('datasources.db') protected db: juggler.DataSource,
@repository.getter('OrderRepository')
getOrderRepository: Getter<OrderRepository>,
) {
super(Customer, db);
this.orders = this.createHasManyRepositoryFactoryFor(
'orders',
getOrderRepository,
);
}
}
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 aPUT
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 onDefaultCrudRepository
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
import {post, param, requestBody} from '@loopback/rest';
import {CustomerRepository} from '../repositories/';
import {Customer, Order} from '../models/';
import {repository} from '@loopback/repository';
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 this.customerRepository.orders(customerId).create(orderData);
}
}
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 requestBody
payload. See its GitHubissue to follow the discussion.