#1.はじめに
この記事は Angular Advent Calendar 2020 17日目の記事です。
スターアプリケーションズ株式会社吉田です。
みなさんNgRxを使っていますか?エッ、あんまり使ってない。そうNgRxは難しいですからね。私もずいぶん苦労しました。いや、仕組みや機能は何とか理解できたのですが、一番難しかったのは、なぜこれを使うのかが分からなかったのです。「頭の良い人が使ってるから?」「みんなが使ってるから?」「チームで統一したいから?」Angularの場合代替手段もあるので、NgRxを使うモチベーションが高まりませんでした。それでもリアクティブプログラミングの流れに乗りたいと思いReduxやFluxも勉強し、使う理由をはっきりさせたかった。
そしてある時、気がついたのです。(ここから先は筆者の主観が多く、間違ってる部分もあるかもしれません。)ReduxにしろNgRxにしろ、世間では「Status管理」とか「状態管理」とよんでいます。決して間違っていないのですが、これが初学者にあらぬ誤解を与えていたのです。例えばNgRxのサンプルでは、カウンターの例が多く出ていますが、たかがカウンターの増減で、どうしてこんなメンドちいことをするのだろう。さらに英語のStatusと日本語のステータスは実は違っているのではと疑ってみました。ステータスというと小さなフラグで✖️✖️✖️Modeとか□□□Modeとかを識別するのに用いたりするものを連想するのですが、Statusはもっと広い世界を現している。結局、ReduxやNgRxが目指しているものは、ページ間で共有するグローバルデータを管理するものだと言うことです。なぜそれが必要か、SPAだからです。エッ!みんな知っていた。失礼しました。
さて、NgRxですがActionやReducerなどを作成するために、AngularCLIを利用したり、いくつかの簡略化の手段が提供されています。それでも多数のentityをstoreで利用する場合には、膨大なコーディングが必要になってきます。そこで以前よりNgRxにFacadeを使う動きがありました(参考)。NgRx DataもNgRxのFacadeとしてとらえられても良いと言うのが、本記事の趣旨です。NgRx Dataを使うことにより、ActionやReducerを書く手間を大幅に省けると言うことです。NgRx Dataを使ったことのない人向けの入門記事にもなります。
#2.デモプログラム
昨年のAdvent CalendarでもNgRxに関して述べました[Angularから@ngrx/dataとActiveRecordを使ってRDBにアクセスする(追記あり)]。昨年のデモでは外部DBを使うものでしたが、本年は簡単なTodoListで内部メモリーだけを使ってAngularが閉じた形になります。
(デモプログラム)
#3. entity
NgRx Dataではentityと言う言葉が何度もでますが、entityとは何でしょうか。非常に端折った表現をすると、一意の識別子(key)を持った表(Table)になります。Tableと言うとデータベースを連想するかもしれませんが、データベースと関連することも有りますが、基本的には無関係です。entityは顧客リストであったり、従業員リストであったり、注文受付記録であったりします。一意の識別子は、NgRxではidと言う列(field)になります。なぜ一意の識別子が必要か、この辺はnrslibさんがDDDの説明で述べている解説がとても参考になります。(ボトムアップドメイン駆動設計 3.2)
entityのデータは必ずしも外部(例えばデータベース)から取り込まれるものに限っている訳ではありません。内部でしか使わないデータでもentityの形式に合わせて持っておくことが出来ます。
#4.セットアップ
##4.1 NgRx Dataのセットアップ
NgRx Dataのセットアップから見ていきます。前提として、store,effects,entityのモジュールがあることです。コンソールから以下を実行します。
- ng add @ngrx/store@latest
- ng add @ngrx/effects@latest
- ng add @ngrx/entity@latest
- ng add @ngrx/data@latest
上記のセットアップが終了すると、(entity-metadata.ts)と言うファイルがappディレクトリーの直下に出来ます。このファイルを編集することにより、entityの登録や、ソート、データの絞り込みなどを宣言することが出来ます。app.modules等への設定は自動的に行われます。
##4.2 entityのセットアップ
entityのセットアップには、3種類(3箇所)あります。例として従業員entity の作成例を見ていきます。
###4.2.1 entity定義
classを作成します。idは必須です。id以外の名前を使うことも出来ます(Entity MetaDataのselectidを参照)。
export class Employee {
id?: number ;
firstName?: string ;
familyName?: string ;
employeeCode?: string ;
divisionCode?: string ;
}
###4.2.2 entity service作成
serviceを作成します。下記の図でEmployeeと記載された部分を各entity毎に修正していきます。
import { Injectable } from '@angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '@ngrx/data';
import { Employee } from './employee' ;
@Injectable({ providedIn: 'root' })
export class EmployeeService extends EntityCollectionServiceBase<Employee> {
constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
super('Employee', serviceElementsFactory);
}
}
###4.2.3 entity登録
entityをNgRx Dataに知らせるため登録します。登録ファイル(entity-metadata.ts)はNgRx Dataインストール時に自動的にappディレクトリの下に作成されます。
import { EntityMetadataMap, EntityDataModuleConfig } from '@ngrx/data';
import { TodoListFilter } from './repository/todo-list/todo-list-filter'
const entityMetadata: EntityMetadataMap = {
Employee :{
//様々なoptionを記載します sort,Filter,id名の変更など
},
};
const pluralNames = {
//複数形が単純に末尾sでない場合、ここに記載します 例 Hero : Heroes
};
export const entityConfig: EntityDataModuleConfig = {
entityMetadata,
pluralNames
};
#5.コマンド
##5.1 コマンドの種類
前節で記載の通りentityごとにserviceが作成され、service毎にコマンドが発行出来ます。コマンドは、発行してそれの応答を待つことはしません。例えばデータを1件削除するコマンドを発行すると、entityのリストの中から1件削除され、その内容を自動的に受け取る形になります。
entityコマンドは次の通りです。
interface EntityCommands<T> extends EntityServerCommands, EntityCacheCommands {
// inherited from data/EntityServerCommands
add(entity: T, options?: EntityActionOptions): Observable<T>
cancel(correlationId: any, reason?: string, options?: EntityActionOptions): void
delete(entity: T, options?: EntityActionOptions): Observable<number | string>
getAll(options?: EntityActionOptions): Observable<T[]>
getByKey(key: any, options?: EntityActionOptions): Observable<T>
getWithQuery(queryParams: string | QueryParams, options?: EntityActionOptions): Observable<T[]>
load(options?: EntityActionOptions): Observable<T[]>
update(entity: Partial<T>, options?: EntityActionOptions): Observable<T>
upsert(entity: T, options?: EntityActionOptions): Observable<T>
// inherited from data/EntityCacheCommands
addAllToCache(entities: T[], options?: EntityActionOptions): void
addOneToCache(entity: T, options?: EntityActionOptions): void
addManyToCache(entities: T[], options?: EntityActionOptions): void
clearCache(options?: EntityActionOptions): void
removeOneFromCache(entity: T, options?: EntityActionOptions): void
removeManyFromCache(entities: T[], options?: EntityActionOptions): void
updateOneInCache(entity: Partial<T>, options?: EntityActionOptions): void
updateManyInCache(entities: Partial<T>[], options?: EntityActionOptions): void
upsertOneInCache(entity: Partial<T>, options?: EntityActionOptions): void
upsertManyInCache(entities: Partial<T>[], options?: EntityActionOptions): void
setFilter(pattern: any, options?: EntityActionOptions): void
setLoaded(isLoaded: boolean, options?: EntityActionOptions): void
setLoading(isLoading: boolean, options?: EntityActionOptions): void
}
上のコマンドですが、Cache系とそれ以外に分かれています。Cache系は文字通り、Cacheのデータに対する操作になります。本記事DemoプログラムはCache上の操作になります。
また上のコマンド軍の中にupsertと言う文字列がありますが、これはinsertとupdateを合成したものになります。更新しに行って、データが存在しなければデータの挿入になります。
パラメータのうちentityとあるのは1行分のデータ、entitiesとあるのは複数行分可能になります。Partialとあるのは部分の意味です。例えば更新系ですと、entityの全てのフィールドを設定することなく一部のフィールド(変更があった部分)のみの設定で可能です。パラメータで注意しなければならないのは、データは必ずentityとは別に作成してそれを渡します。
##5.2 コマンドの使い方
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
@Component({
selector: 'app-employees',
templateUrl: './employees.component.html',
styleUrls: ['./employees.component.scss']
})
export class EmployeesComponent implements OnInit {
loading$: Observable<boolean>;
employees$: Observable<Employee[]>;
constructor(private employeeService: EmployeeService) {
this.employees$ = employeeService.entities$;
this.loading$ = employeeService.loading$;
}
ngOnInit() {
this.getEmployees();
}
add(employee: Employee) {
this.employeeService.add(employee);
}
getEmployees() {
this.employeeService.getAll();
}
update(employee: Employee) {
this.employeeService.update(employee);
}
#6.データ受け取り
前節でコマンド発行、この節でデータの受け取りと言うことで、何となくCQRSの流れになっていると思いませんか。
##6.1 受け取りデータの種類
NgRx Dataで受け取るのは次の3つです。特に最初の2つが重要です。末尾が$の変数はObservableな変数になります。
・entities$ entityの全データが対象になります
・filteredEntities$ 予めfilterで絞り込まれたデータが対象です
・loading$ boolean値で現在ロード中ならtrueになりまう
##6.2 データ取得
component側ではserviceをDIで取得、entityの値とobservable変数を結びつけます。Template側では、Asyncパイプを使ってデータを取り込みます。
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
@Component({
selector: 'app-employees',
templateUrl: './employees.component.html',
styleUrls: ['./employees.component.scss']
})
export class EmployeesComponent implements OnInit {
loading$: Observable<boolean>;
employees$: Observable<Employee[]>;
constructor(private employeeService: EmployeeService) {
this.employees$ = employeeService.entities$;
Template側は次のようになります。
<ng-container *ngIf = "employees$ | async as employees">
<!-- ユーザーのHTML文 *ngIf が成功したとき実行される -->
</ng-container>
#7.まとめ
本記事は、NgRx Dataのマニュアルに沿った内容になります。ここで、プログラムの内部データや設定値のようなものでも、entityの形式に落とし込めれば、NgRx Dataを利用することができると言うことです。
もう一度NgRx Dataの内容をまとめると、次の5つのことを行うだけです。
1.NgRx Dataをprojectにセットアップする
2.entityの定義を行い、登録する
3.entity serviceを作成する(ボイラープレート有り)
4.entity serviceを使ってコマンドを発行する
5.Asyncパイプでデータを受け取る(将来は Let Directiveになる可能性も)
上記の2から5はentityごとに発生するものです。基本の形が用意されているので、うまく利用すれば開発の生産性がとても上がってくると思います。