13
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AngularAdvent Calendar 2020

Day 17

NgRx Dataが便利で、便利で

Last updated at Posted at 2020-12-16

#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を参照)。

employee.ts
export class Employee {
    id?: number ;
    firstName?: string ;
    familyName?: string ;
    employeeCode?: string ;
    divisionCode?: string ; 
}

###4.2.2 entity service作成
 serviceを作成します。下記の図でEmployeeと記載された部分を各entity毎に修正していきます。

employee.service.ts
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ディレクトリの下に作成されます。

entity-metadata.ts
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パイプを使ってデータを取り込みます。

employees.component.ts
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側は次のようになります。

employees.component.html

<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ごとに発生するものです。基本の形が用意されているので、うまく利用すれば開発の生産性がとても上がってくると思います。

13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?