@ngrx/data は、アプリケーションの CRUD 処理を強力にアシストしてくれる魔法のようなライブラリです。
2019年に公式リリースされたばかりですが、やはり後発だけあって非常に使いやすいので、ぜひこの場でご紹介したいな、と思った次第です。
Angular アプリの CRUD 処理は、@ngrx/data で決まり!
今回のテーマは @ngrx/data だよ。
かなりの長丁場になるので、いくつかに分けて話そうと思う。
あれ??
先日、NgRx は難しいって話してなかったけ?
したよ。
NgRx シリーズには、もともと Redux をベースにした @ngrx/store, @ngrx/effects などがあって、これらは確かに使いこなすのが大変なんだ。
でも、@ngrx/data だけは毛色が違う。
@ngrx/data を使用すると、非常に少ないコードで、NgRx をまったく知らなくても、大規模なエンティティモデルを迅速に開発できます。
【原文】With NgRx Data you can develop large entity models quickly with very little code and without knowing much NgRx at all.【出典】
ちなみに、NgRx にありがちな リアルタイム Web とか、プッシュ通知とか、状態管理とか、そういうおしゃれな話は一切出てきません。
業務アプリケーションにおいてとても普遍的な CRUD 処理をラクに実装するためのアプローチとして、@ngrx/data を使ってみませんか、という趣旨です。
言い換えよう。 この際、NgRx の知識は要らないんだ。
すごく便利なライブラリがあるからステップバイステップで試していこう!
うん。
面倒なところはほとんど全てライブラリが内部的に処理してくれる。 オートマ車みたいだ。
補足: CRUD 処理とトランザクション制御について
通常、関係データベースから取得したデータ群は、アプリケーションの変数に一時的にキャッシュされます。
このとき、データベースとキャッシュの整合性をきちんと維つことがポイントになってきます。
たとえばデータベースへの書き込みに失敗したときは、アプリケーションでキャッシュしている変数の値もロールバックしなくてはなりません。
さもないと、データベースとキャッシュに差異が出てしまい、思わぬバグの原因になりかねないからです。
これは、トランザクションが複雑になればなるほど、難しい課題として開発者を苦しめます。
例えば、同じレコードを複数のユーザがほぼ同時に更新しようとしてコミットが競合した場合を考えてみよう。
アプリ側のキャッシュに更新できなかったデータが残り続けたらおかしくなっちゃうでしょ?
@ngrx/data がやってくれること
実習に入る前に、ちょっと @ngrx/data について基本的なところを押さえておこう。
@ngrx/data がやってくれることは、下図の 1〜9 ほぼ全てだよ。
- The view/component calls EntityCollectionService.getAll(), which dispatches the hero's QUERY_ALL EntityAction to the store.
- NgRx kicks into gear ...
- The NgRx Data EntityReducer reads the action's entityName property (Hero in this example) and forwards the action and existing entity collection state to the EntityCollectionReducer for heroes.
- The collection reducer picks a switch-case based on the action's entityOp (operation) property. That case processes the action and collection state into a new (updated) hero collection.
- The store updates the entity cache in the state tree with that updated collection.
- NgRx observable selectors detect and report the changes (if any) to subscribers in the view.
- The original EntityAction then goes to the EntityEffects.
- The effect selects an EntityDataService for that entity type. The data service sends an HTTP request to the server.
- The effect turns the HTTP response into a new success action with heroes (or an error action if the request failed).
- NgRx effects Dispatches that action to the store, which reiterates step #2 to update the collection with heroes and refresh the view.
【出典】
この連載の目標は、@ngrx/data を使えるようになること
今回の環境
共通手順:プロジェクトの作成と初期設定
まずは、すべての出発点となる環境設定手順を以下に示すよ。
@ngrx/data を使う場合、プロジェクトごとにほぼ毎回必要な手順になります。
コマンドのオプションは、できる限りデフォルトのものを用いているよ。
最初はこの通りに打ち込んで、慣れてきたら少しずつ好きな構成にアレンジしてみよう。
プロジェクトの作成
$ ng new NgRxDataSample --routing --style scss
ちなみに、この NgRxDataSample
の部分はプロジェクト名です。
べつに絶対コレじゃなきゃダメというわけではなく、任意の名前で作成可能だよ。
コマンドが正常終了したら、cd
でプロジェクトフォルダ(ここではNgRxDataSample
)へ移動しましょう。
$ cd ./NgRxDataSample
ライブラリを追加するため、以下のコマンドを順に打ち込んでいきます。
$ ng add @ngrx/store $ ng add @ngrx/store-devtools $ ng add @ngrx/effects $ ng add @ngrx/entity $ ng add @ngrx/data
※ 普通はここで問題なく成功するのですが、2020年2月11日現在、どうやらエラーになってしまうようです。
あれ…… なんかいきなり失敗したぞ。
package.json
を見ると、明らかに古すぎるバージョンを入れようとしているっぽい。
$ ng add @ngrx/store@latest
パッケージ名の後ろに、スペースを開けず @latest
と続けるんだよ。
Angular のメジャーバージョンが上がったばっかりなせいか、依存関係の解決がちょっと不安定だね。
警告も出ているし。
ここはそのうちきちんと対策されるだろうと思っているから、とりあえず先に進もうか。
ところで、こんなに一度にたくさんのコマンドを打ち込んで大丈夫だったのかな? 何が起きたか心配なんだけど。
先ほどの図にもある通り、@ngrx/data を動かすためには、@ngrx/store, @ngrx/effects, @ngrx/entity が必須なんだ。
@ngrx/store-devtools は、入れても入れなくてもどっちでもいいけど、あると便利だから敢えて入れてる。
まぁね。 車を走らせるためにはとりあえずガソリンを入れるでしょ?
そのとき、なんでガソリンが必要なのかなんていちいち考えないでしょ?
なんか今日は自動車の喩え話が多いね。
車でも買うの?
一連のng add
コマンドによって何が起きたか
ちょっと、この一連の操作によってプロジェクトの身に何が起きたかを確認していこう。
興味がなければ読み飛ばしてもいいけど。
コマンド実行後のフォルダ階層を見てみよう。
ファイルやフォルダ名の右側に アイコンが付いているのは、コマンドによって新しく作られたもの、 アイコンは書き換えられたものを示しているよ。
変更された package.json
をエディタで開いてみよう。
dependencies
に 5 つのパッケージが追加されていることが分かるね。
package.json(差分)
"dependencies": { "@angular/animations": "~9.0.0", "@angular/common": "~9.0.0", "@angular/compiler": "~9.0.0", "@angular/core": "~9.0.0", "@angular/forms": "~9.0.0", "@angular/platform-browser": "~9.0.0", "@angular/platform-browser-dynamic": "~9.0.0", "@angular/router": "~9.0.0", + "@ngrx/data": "^8.6.0", + "@ngrx/effects": "^8.6.0", + "@ngrx/entity": "^8.6.0", + "@ngrx/store": "^8.6.0", + "@ngrx/store-devtools": "^8.6.0", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" },
あとは app.module.ts
の imports
にいくつか自動でコードが追加されている。
NgRx を直に弄るなら知っておいた方がよいものもあるけど、今のところ特に気にする必要はない。
app.module.ts(差分のみ)
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, - AppRoutingModule + AppRoutingModule, + EffectsModule.forRoot([AppEffects]), + StoreModule.forRoot(reducers, { + metaReducers, + runtimeChecks: { + strictStateImmutability: true, + strictActionImmutability: true + } + }), + StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), + EntityDataModule.forRoot(entityConfig) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
共通手順:app.module.ts に HttpClientModule を import する
main.ts:12 NullInjectorError: R3InjectorError[EntityDataModule -> EntityCacheEffects -> EntityCacheDataService -> HttpClient -> HttpClient -> HttpClient]:
NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:4200/vendor.js:10919:27)
at R3Injector.get (http://localhost:4200/vendor.js:24553:33)
at R3Injector.get (http://localhost:4200/vendor.js:24553:33)
at R3Injector.get (http://localhost:4200/vendor.js:24553:33)
at injectInjectorOnly (http://localhost:4200/vendor.js:10774:33)
at Module.ɵɵinject (http://localhost:4200/vendor.js:10784:57)
at Object.EntityCacheDataService_Factory [as factory] (http://localhost:4200/vendor.js:65667:252)
at R3Injector.hydrate (http://localhost:4200/vendor.js:24779:63)
at R3Injector.get (http://localhost:4200/vendor.js:24541:33)
at injectInjectorOnly (http://localhost:4200/vendor.js:10774:33)
app.module.ts
の imports
に HttpClientModule
を追加しよう。app.module.ts(差分のみ)
+ import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, + HttpClientModule, EffectsModule.forRoot([AppEffects]), StoreModule.forRoot(reducers, { metaReducers, runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true } }), StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), EntityDataModule.forRoot(entityConfig) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
imports
の中なら、どこに追加してもいいの?HttpClientModule
は EffectsModule
よりも先に書かないとダメ。ng serve
を打ち込んでアプリケーションを実行してみよう。おめでとう。
今回は、プロジェクトを作ってライブラリを入れて、エラーを直してとりあえず動く状態にしたね。
激闘編につづく。