This relation best works with databases that support foreign key and uniqueconstraints (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 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 relation denotes a one-to-one connection of a model to another modelthrough referential integrity. The referential integrity is enforced by aforeign key constraint on the target model which usually references a primarykey on the source model and a unique constraint on the same column/key to ensureone-to-one mapping. This relation indicates that each instance of the declaringor source model has exactly one instance of the target model. Let’s take anexample where an application has models Supplier
and Account
and aSupplier
can only have one Account
on the system as illustrated in thediagram below.
The diagram shows target model Account has property supplierId as theforeign key to reference the declaring model Supplier’s primary key id.supplierId needs to also be used in a unique index to ensure eachSupplier has only one related Account instance.
To add a hasOne
relation to your LoopBack application and expose its relatedroutes, you need to perform the following steps:
- Decorate properties on the source and target models with
@hasOne
and@belongsTo
to let LoopBack gather the neccessary metadata. - 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.Right now, LoopBack collects the neccessary metadata and exposes the relationAPIs for the
hasOne
relation, but does not guarantee referential integrity.This has to be set up by the user or DBA in the underlying database and anexample is shown below on how to do it with MySQL.
This section describes how to define a hasOne
relation at the model levelusing the @hasOne
decorator. The relation constrains the target repository bythe foreign key property on its associated model. The hasOne
relation isdefined on a source model Supplier
in the example below:
/src/models/supplier.model.ts
import {Supplier, SupplierWithRelations} from './supplier.model';
import {Entity, property, belongsTo} from '@loopback/repository';
export class Account extends Entity {
@property({
type: 'number',
id: true,
})
id: number;
@property({
type: 'string',
})
accountManager: string;
@belongsTo(() => Supplier)
supplierId: number;
constructor(data: Partial<Account>) {
super(data);
}
export interface AccountRelations {
supplier?: SupplierWithRelations;
}
export type AccountWithRelations = Account & AccountRelations;
The definition of the hasOne
relation is inferred by using the @hasOne
decorator. The decorator takes in a function resolving the target model classconstructor and optionally a has one relation definition object which can e.g.contain a custom foreign key to be stored as the relation metadata. Thedecorator logic also designates the relation type and tries to infer the foreignkey 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).
The decorated property name is used as the relation name and stored as part ofthe source model definition’s relation metadata.
A usage of the decorator with a custom foreign key name for the above example isas follows:
At the moment, LoopBack does not provide the means to enforce referentialintegrity for the hasOne
relation. It is up to users to set this up at thedatabase layer so constraints are not violated. Let’s take MySQL as the backingdatabase for our application. Given the Supplier
has one Account
scenarioabove, we need to run two SQL statements on the Account
table for the databaseto enforce referential integrity and align with LoopBack’s hasOne
relation.
- Make
supplierId
property or column a foreign key which references theid
from Supplier model’sid
property:
ALTER TABLE <databaseName>.Account ADD FOREIGN KEY (supplierId) REFERENCES <databaseName>.Supplier(id);
- Create a unique index for the same property
supplierId
, so that for eachSupplier
instance, there is only one associatedAccount
instance.
Before making the following changes, please follow the steps outlined inDatabase Migrations to create the database schemasdefined by the models in your application.
The configuration and resolution of a hasOne
relation takes place at therepository level. Once hasOne
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, use 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
Account
to break a cyclicdependency between a repository with a hasOne relation and a repository withthe matching belongsTo relation.Call the
createHasOneRepositoryFactoryFor
function in the constructor of thesource repository class with the relation name (decorated relation property onthe source model) and target repository instance and assign it the propertymentioned above.
The following code snippet shows how it would look like:
/src/repositories/supplier.repository.ts
import {Account, Supplier, SupplierRelations} from '../models';
import {AccountRepository} from './account.repository';
import {
DefaultCrudRepository,
juggler,
HasOneRepositoryFactory,
repository,
} from '@loopback/repository';
export class SupplierRepository extends DefaultCrudRepository<
Supplier,
typeof Supplier.prototype.id,
SupplierRelations
> {
public readonly account: HasOneRepositoryFactory<
Account,
typeof Supplier.prototype.id
>;
constructor(
@inject('datasources.db') protected db: juggler.DataSource,
@repository.getter('AccountRepository')
getAccountRepository: Getter<AccountRepository>,
) {
super(Supplier, db);
this.account = this.createHasOneRepositoryFactoryFor(
'account',
getAccountRepository,
);
}
}
The following CRUD APIs are now available in the constrained target repositoryfactory Account
for instances of supplierRepository
:
create
for creating anAccount
model instance belonging toSupplier
model instance(API Docs)get
finding the target model instance belonging toSupplier
model instance()
The same pattern used for ordinary repositories to expose their CRUD APIs viacontroller methods is employed for hasOne
repositories. Once the hasOnerelation 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/supplier-account.controller.ts
In LoopBack 3, the REST APIs for relations were exposed using static methodswith the name following the pattern {methodName}{relationName}
(e.g.Supplier.
find__account
). 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 Account
-related methods to SupplierController
, but instead createa new SupplierAccountController
class for them.
The type of accountData
above will possibly change to to excludecertain properties from the JSON/OpenAPI spec schema built for the requestBody
payload. See its to follow the discussion.