Loopback4ではモデルを作ってAPIを公開するところまでは簡単な作業でできるが、モデル間の関係を表そうとするにはちょっと難しいオマジナイがいるようで、APIにどう公開するかも色々ありそうだった。
物品管理を例に、物品を表すItemモデルと、入れ物を表すContainerモデルを使って関係を表現してみる。(関係を表す矢印があるとしたら、Item=Source、Container=Targetになる)
18/11/4時点で、HasManyは正しく動かない(https://github.com/strongloop/loopback-next/issues/1944 )。すぐ使いたければ一時的にBelongsToでやった方が良さそう。
BelongsTo
物品を中心に「どこに入っているか」を知る場合、ItemのプロパティにItemが属するContainerの情報があればよい。このような関係はbelongsToの関係で表せる。
LooBack4のドキュメント(https://loopback.io/doc/en/lb4/BelongsTo-relation.html) によると、モデル・リポジトリ・コントローラそれぞれでやることがあるらしい。
ソース側のモデルに外部キー制約をかける
ソース側(Item
)モデルのプロパティに外部キー制約をかけるには、外部キーのプロパティに@belongsTo( () => targetClass)
デコレータを付ける。デフォルトでターゲットのid
プロパティを主キーと扱うが、主キー名を変えていたら@belongsTo( () => targetClass, { keyTo: '主キー名'}
で指定する。
もしかしたら現時点では未実装? 存在しないIDも追加できた。
import { model, property, belongsTo } from '@loopback/repository';
import { CommonModel, Container } from '.';
// CommonModelでidプロパティを定義している
@model()
export class Item extends CommonModel {
@belongsTo( () => Container )
containerId: number;
constructor(data?: Partial<Item>) {
super(data);
}
}
プロパティの型に
targetClass.prototype.id
のように、対象の型を設定することで矛盾を防げるかと思ったが、やってみたらObject扱いになってしまったのでNG。
リポジトリにBelongsToAccessorを追加する
LoopBack4では、Itemが属するContainerをItemのIDで取り出すためのBelongsToAccessor
を作る手段が用意されている。これにより、例えばitemRepository.container( itemId )
のようにアクセスできるようになる。
外部キー
Item.containerId
が分かれば、Containerリポジトリから外部キーを使ってContainerを取り出すことはできるから必須ではない。とはいえ、あった方が便利なんだと思う。
BelongsToAccessorを作るには、
- BelongsToAccessorとなるメンバを宣言する
型はBelongsToAccessor<ターゲットの型, 検索に使うIDの型>
で指定する。ここでは、ターゲットはContainerモデルで、検索に使うIDの型はItemモデルのIDの型としてtypeof Item.prototype.id
を指定している。 - ターゲットのRepository.getter関数をDependency Injectionする
@repository.getter('ターゲット側リポジトリ名') getter名: Getter<ターゲット側リポジトリ>
をコンストラクタの引数に追加する。リポジトリをDependency Injectionしないのは、リポジトリ間でbelongsToとhasManyがあった際に循環参照になるのを防ぐための工夫らしい。(AngularはDIの循環参照をうまく解決していた気がするけど) - BelongsToAccessorを定義する
ファクトリ関数を使ってthis._createBelongsToAccessorFor( ソース側の外部キー名, Repository.getter関数 )
のように定義する。ここでは、ソース側の外部キーはcontainerId
、Repository.getter関数はcontainerRepositoryGetter
を指定している。
import { DefaultCrudRepository, juggler, BelongsToAccessor, repository } from '@loopback/repository';
import { Item, Container } from '../models';
import { SqldbDataSource } from '../datasources';
import { ContainerRepository } from './container.repositorie';
import { inject, Getter } from '@loopback/core';
export class ItemRepository extends DefaultCrudRepository<Item,typeof Item.prototype.id> {
[1] public readonly container: BelongsToAccessor<Container,
typeof Item.prototype.id>;
constructor(@inject('datasources.sqldb')
dataSource: SqldbDataSource,
[2] @repository.getter('ContainerRepository')
containerRepositoryGetter: Getter<ContainerRepository>) {
super(Item, dataSource);
[3] this.container = this._createBelongsToAccessorFor('containerId', containerRepositoryGetter);
}
}
うっかり忘れがちだが、リポジトリ間で参照しあう場合は、importでは直接ファイルを指定した方が良い。import { SomeRepository } from '.'のようにすると、index.ts内のexportの順序次第で不可解なコンパイルエラーで悩まされることになる。
コントローラでBelongsToAccessorを使う
Itemが属するContainerをどのように利用者に提供するかはAPIの設計次第らしい。例えば、
- belongsTo用のエンドポイントを設ける
- Itemのエンドポイント
/item/{id}
に対して、/item/{id}/container
エンドポイントを設ける
いずれの場合も、Containerの取得に先ほど作成したBelongsToAccessorを使うので、例えば[2]の場合は以下のようになる。
@get('/orders/{id}/container')
async getContainer( @param.path.number('id') id: typeof Item.prototype.id ): Promise<Container> {
// ここで先ほど作ったリポジトリのBelongsToAccessorを使って、Itemに対応するContainerを取得
return await this.itemRepository.container( id );
}
後で、「ItemのGet時に自動でContainerを取得して返す」を試そうとしたところ、応答にContainerを付けても消されてしまった。LoopBack4の思想的にそうするものではないのかもしれない。(LoopBack3では、リレーションごとにエンドポイントを設けることになっていたみたいなので)
HasMany
1対多の関係を表すので、試しにContainer hasMany Items という関係を表してみようとしたところ、以下のようになってしまった。
BelongsToと同じようになるのかと思ったら期待しない動きになってしまった。itemsプロパティに配列が直接格納されてしまい、Itemモデルのテーブルとは無関係になっている。
調べたところ、バグの様だった。この記事を書く5日前にIssueとして挙がっていた。(https://github.com/strongloop/loopback-next/issues/1944)
現状はLoopBack4自体が正式リリースではないから仕方なさそう。