はじめからはじめる @ngrx/data 第2話(NgRx 激闘編)

今日も、@ngrx/data の続きです。

前回は @ngrx/data が何なのかをくどくど説明して、プロジェクトをセットアップして、ライブラリをブチ込んだところで時間切れでした。

今日はいよいよ本格的に @ngrx/data に挑戦します。


前回の話
tercel-tech.hatenablog.com

例題

次のエンティティに対する CRUD 処理を @ngrx/data で実装してみよう。ちなみに主キーは id だよ。

エンティティ名: Hero

項目名
id number
name string

まずはこのエンティティを表現するクラスが必要だね。 DTO みたいなやつが。


エンティティクラスを作ろう

それじゃ、Hero エンティティのための Hero クラスを作ろう。

ちなみにエンティティ名は、今回の例に限らず基本的に名詞の単数形にしておくと吉だよ。


$ ng g class Hero --skipTests

コマンドの最後の --skipTests って何?

余計な spec.ts が作られなくなるおまじない。

単なるデータのれ物は、単体テストを書く意味があまりないからね。

src/app/hero.ts

export class Hero {
  id: number;
  name: string;
}

エンティティメタデータの編集

次に、@ngrx/data に Hero エンティティの存在を教えてあげなきゃいけない。


src/app/entity-metadata.ts を開いて、以下のように編集しましょう。

  import { EntityMetadataMap, EntityDataModuleConfig } from '@ngrx/data';

  const entityMetadata: EntityMetadataMap = {
+   Hero: { } // ❶
  };

  const pluralNames = {
+   Hero: 'Heroes'  // ❷
  };

  export const entityConfig: EntityDataModuleConfig = {
    entityMetadata,
    pluralNames
  };


❶ を書くことで、先ほど作った Hero クラスが @ngrx/data に登録されるよ。

とりあえず、 { } の中は何も書かなくていい

どういうときに { } の中身が必要なの?

レコードを絞ったり(WHEREみたいなやつ)、ソートしたり(ORDER BYみたいなやつ)。

あと、主キーの項目名が id 以外の場合は、ここで主キーを指定する必要があるよ。

❷ は エンティティ名 Hero の複数形を指定しているんだね。


補足: エンティティの主キーの話

デフォルトでは、暗黙的に id という名の項目がエンティティの主キーと見なされるんだよ。

だから、今回の例では ❶ には特になにも指定していない。

id 以外の項目も主キーにできるんだよね?

うん。 でも複合主キーは基本的に許されない

たとえば、多対多の関係性を取り持つ連関エンティティの場合も、ちゃんと単独で機能する主キーが別途必要になるんだ。

ここはエンティティ設計に影響を与えそうなポイントだから、ちょっと注意が必要だね。


補足: エンティティ名の単数形と複数形

❷ の plural は、複数形という意味だよ。

@ngrx/data は、API 設計的に単数形と複数形を区別しているんだ。

どういうこと?

ちょっと先回りすると、Hero エンティティの一覧を取得しようとしたとき、@ngrx/data は自動で /api/heroes/ という URL に HTTP Get リクエストを投げる。

URL が複数形になってるね。

一方、id = 1 みたいに、主キーを指定して単一のレコードを削除しようとしたときは、 /api/hero/1 という URL に HTTP Delete を投げる。

こっちは URL が単数形だね。

まぁそんな感じで、@ngrx/data は対象が単数形か複数形かに応じて Web API の URL を打ち分けるんだよ。

なるほど、わかりやすい。

しかも、この複数形の URL は、エンティティ名に基づいて @ngrx/data が自動生成してくれる。

だからさっき「エンティティ名は名詞の単数形で」って言ってたのか。

でもタネを明かすと、 @ngrx/data がやっていることは、機械的にエンティティ名のうしろに〝s〟をつけてるだけなんだ。

たとえば Book というエンティティ名の複数形は Books みたいにね。

単純にエンティティ名のうしろに〝s〟をつけるだけではダメな場合は、@ngrx/data に教えてやる必要があるんだね。

Hero と Heroes とか、Octopus と Octopi とか。

そうそう。そういうのを ❷ に指定するんだ。


サービス経由でエンティティを操る

ここまでで、だいたい前準備はおしまい。

コードよりも周辺の会話の方が多かったね。

いよいよコンポーネントから、@ngrx/data のエンティティサービス経由で Hero エンティティを操作する方法を見ていこう。

まず、サービスを使えるようにしないとね。

いろいろな書き方があるけれど、一番簡単なのはコレかな。

❶ 
コンストラクタに EntityServicesインスタンスを DI して、
❷ 
EntityServices#getEntityCollectionService(エンティティ名) で当該エンティティのサービスを取得する。


src/app/app.component.ts

import { Component } from '@angular/core';
import { EntityCollectionService, EntityServices } from '@ngrx/data';
import { Hero } from './hero';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  private heroService: EntityCollectionService<Hero>;

  constructor(entityServices: EntityServices) { // ❶
    this.heroService = entityServices.getEntityCollectionService<Hero>('Hero'); // ❷
  }
}

たとえばもし、 Villain というエンティティのためのサービスを取得したければ、上記の Hero をすべて Villain に換えればいいんだね。

そういうこと。

❷ で取得したサービスは heroService に代入している。

この heroService を通して CRUD 操作を行えばいいの?

うん。

代表的なメソッドを以下にまとめたよ。


METHOD MEANING HTTP METHOD WITH ENDPOINT
add(entity: T) 新しいエンティティを追加します POST /api/hero/
delete(id: any) 主キーの値でエンティティを削除します DELETE /api/hero/5
getAll() このエンティティタイプのすべてのインスタンスを取得します GET /api/heroes/
getById(id: any) 主キーでエンティティを取得します GET /api/hero/5
getWithQuery(queryParams: QueryParams | string) クエリを満たすエンティティを取得します GET /api/heroes/?name=bombasto
update(update: Update) 既存のエンティティを更新します PUT /api/hero/5


もし、this.heroService.getAll() を実行すると、自動的に /api/heroes/ に向けて HTTP Get が飛ぶよ。

HTTP 通信を飛ばすところはサービスの中に隠されているんだね。

ちなみに、さっきの単数形と複数形の話を思い出して。

API によって URL が単数形だったり複数形だったりするでしょ。

ホントだ。

なので、もしサーバサイド API を後から作るような場合は、この @ngrx/data の命名規約に合わせるのがいいと思う。

明快で分かりやすいし。

ちなみにこの URL って変えられないの?

たとえば、必ずしも /api 始まりじゃないかも知れないし、もっと言うとドメインすら違うかも知れない。

もちろん変更できるよ。

それならいいや。

ところで、HTTP リクエストは飛ぶんだけど、今のところその先には何も無いから普通に 404 Not Found エラーになる。

それじゃあ、まだ動かして試せないってこと?

普通は DB を作って、PHP でも Node.js でもなんでもいいからサーバサイドの API 実装を書く必要があるんだけど……ぶっちゃけやってらんないでしょ?

うん。 もっと手っ取り早く動かしたいね。

だから、こういう Web API を Angular 内でエミュレートできる Angular In Memory Web API っていうものがあるので、それを使っていこう。

まぁ、続きは次回だね。

今回書いたコードなんて実質10行くらいなのにこの記事の長さ。

コード量は少ないけど、一つひとつが意味深調だからどうしてもくどくなっちゃうんだよね。

次回は、サーバサイド処理をエミュレートする Angular In Memory Web API について集中的に取り上げていくよ。

ではまた。

Angular In Memory Web API 編につづく。

tercel-tech.hatenablog.com

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.