Overview

Note:

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 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 hasOne 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.

hasOne relation illustration

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.

Defining a hasOne Relation

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

  1. import {Account, AccountWithRelations} from './account.model';
  2. import {Entity, property, hasOne} from '@loopback/repository';
  3. export class Supplier 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. @hasOne(() => Account)
  15. account?: Account;
  16. constructor(data: Partial<Supplier>) {
  17. super(data);
  18. }
  19. }
  20. export interface SupplierRelations {
  21. account?: AccountWithRelations;
  22. }
  23. export type SupplierWithRelations = Supplier & SupplierRelations;

On the other side of the relation, we’d need to declare a belongsTo relationsince every Account has to belong to exactly one Supplier:

  1. import {Supplier, SupplierWithRelations} from './supplier.model';
  2. import {Entity, property, belongsTo} from '@loopback/repository';
  3. export class Account extends Entity {
  4. @property({
  5. type: 'number',
  6. id: true,
  7. })
  8. id: number;
  9. @property({
  10. type: 'string',
  11. })
  12. accountManager: string;
  13. @belongsTo(() => Supplier)
  14. supplierId: number;
  15. constructor(data: Partial<Account>) {
  16. super(data);
  17. }
  18. }
  19. export interface AccountRelations {
  20. supplier?: SupplierWithRelations;
  21. }
  22. export type AccountWithRelations = Account & AccountRelations;

The definition of the hasOne relation is inferred by using the @hasOnedecorator. 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:

  1. // import statements
  2. class Supplier extends Entity {
  3. // constructor, properties, etc.
  4. @hasOne(() => Account, {keyTo: 'suppId'})
  5. account?: Account;
  6. }

Setting up your database for hasOne relation - MySQL

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 the idfrom Supplier model’s id property:
  1. 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 associated Account instance.
  1. ALTER TABLE <databaseName>.Account ADD UNIQUE INDEX supplierIndex (supplierId);

Before making the following changes, please follow the steps outlined inDatabase Migrations to create the database schemasdefined by the models in your application.

Configuring a hasOne relation

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, 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 Account to break a cyclicdependency between a repository with a hasOne relation and a repository withthe matching belongsTo relation.

  • Declare a property with the factory function typeHasOneRepositoryFactory<targetModel, typeof sourceModel.prototype.id> on thesource repository class.

  • 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

  1. import {Account, Supplier, SupplierRelations} from '../models';
  2. import {AccountRepository} from './account.repository';
  3. import {
  4. DefaultCrudRepository,
  5. juggler,
  6. HasOneRepositoryFactory,
  7. repository,
  8. } from '@loopback/repository';
  9. import {inject, Getter} from '@loopback/core';
  10. export class SupplierRepository extends DefaultCrudRepository<
  11. Supplier,
  12. typeof Supplier.prototype.id,
  13. SupplierRelations
  14. > {
  15. public readonly account: HasOneRepositoryFactory<
  16. Account,
  17. typeof Supplier.prototype.id
  18. >;
  19. constructor(
  20. @inject('datasources.db') protected db: juggler.DataSource,
  21. @repository.getter('AccountRepository')
  22. getAccountRepository: Getter<AccountRepository>,
  23. ) {
  24. super(Supplier, db);
  25. this.account = this.createHasOneRepositoryFactoryFor(
  26. 'account',
  27. getAccountRepository,
  28. );
  29. }
  30. }

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

  • create for creating an Account model instance belonging to Suppliermodel instance(API Docs)
  • get finding the target model instance belonging to Supplier model instance(API Docs)

Using hasOne constrained repository in a controller

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

  1. import {post, param, requestBody} from '@loopback/rest';
  2. import {SupplierRepository} from '../repositories/';
  3. import {Supplier, Account} from '../models/';
  4. import {repository} from '@loopback/repository';
  5. export class SupplierAccountController {
  6. constructor(
  7. @repository(SupplierRepository)
  8. protected supplierRepository: SupplierRepository,
  9. ) {}
  10. @post('/suppliers/{id}/account')
  11. async createAccount(
  12. @param.path.number('id') supplierId: typeof Supplier.prototype.id,
  13. @requestBody() accountData: Account,
  14. ): Promise<Account> {
  15. return this.supplierRepository.account(supplierId).create(accountData);
  16. }
  17. }

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.

Note:

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