原文はこちら。これをざっくり訳したものです。
3. Collections in AngularFirestore
Cloud Firestoreは、ドキュメント指向のNoSQLデータベースです。SQLデータベースと異なり、テーブルや行といった概念がありません。その代わりに、documentsというものにデータを保存し、それらが集まってcollectionsという形で保持されます。各documentは、key-valueペアが保持され、Cloud Firestoreは、小さなdocumentからなる巨大なcollectionを保存することに最適化されています。
AngularFirestoreCollection
を利用する
AngularFirestoreCollection
サービスは、Firestore SDKのCollectionReference
と Query
typesをラップしたものです。
強力なデータ操作・ストリーミングの機能を提供するジェネリックサービスです。
このサービスは、@Injectable()
として利用できるようになっています。
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import 'firebase/firestore';
export interface Item { name: string; }
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let item of items | async">
{{ item.name }}
</li>
</ul>
`
})
export class AppComponent {
private itemsCollection: AngularFirestoreCollection<Item>;
items: Observable<Item[]>;
constructor(private afs: AngularFirestore) {
this.itemsCollection = afs.collection<Item>('items');
this.items = this.itemsCollection.valueChanges();
}
addItem(item: Item) {
this.itemsCollection.add(item);
}
}
AngularFirestoreCollection
は、collectionストリームの作成や基底となるcollectionでのデータ操作の実行に用います。
DocumentChangeAction
タイプ
valueChanges()
を除き、各ストリーミングのメソッドは、DocumentChangeAction[]
からなるObservableを返します。
DocumentChangeAction
は、type
プロパティと payload
プロパティを持っています。type
プロパティは、いつ何の DocumentChangeType
操作(added
, modified
, removed
)が発生したかを表します。payload
プロパティは、変更に関するメタデータと doc
プロパティ(これは DocumentSnapshot
)を含みます。
interface DocumentChangeAction {
//'added' | 'modified' | 'removed';
type: DocumentChangeType;
payload: DocumentChange;
}
interface DocumentChange {
type: DocumentChangeType;
doc: DocumentSnapshot;
oldIndex: number;
newIndex: number;
}
interface DocumentSnapshot {
exists: boolean;
ref: DocumentReference;
id: string;
metadata: SnapshotMetadata;
data(): DocumentData;
get(fieldPath: string): any;
}
collection dataをストリーミングする
Firesotreからのcollectionデータをストリーミングする方法は、複数あります。
valueChanges({idField?: string})
メソッド
What is it? - 現在のcollectionの状態です。JSONオブジェクトからなる配列としてObservableを返します。Snapshotのメタデータは含まれず、document データだけが含まれています。 オプションとして、 idField
キーを文字列型として渡すことができます。その場合、 idField
によって指定されたプロパティにdocument IDがマップされたJSONオブジェクトが返ってきます。
Why would you use it? - 単にデータのリストが欲しい場合など。返ってくる配列にはメタデータが含まれないため、Viewへのレンダリングがシンプルになります。
When would you not use it? - 配列よりも複雑な構成が必要な場合には、使うべきではないです。
Best practices - 単一のページにデータを表示させる際に使いましょう。シンプルかつ効果的です。より複雑になりそうな場合には、.snapshotChanges()
を使いましょう。
Document IDを永続化する例
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import 'firebase/firestore';
export interface Item { id: string; name: string; }
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let item of items | async">
{{ item.name }}
</li>
</ul>
`
})
export class AppComponent {
private itemsCollection: AngularFirestoreCollection<Item>;
items: Observable<Item[]>;
constructor(private readonly afs: AngularFirestore) {
this.itemsCollection = afs.collection<Item>('items');
// .valueChanges() はシンプルで、メタデータなしのJSONデータを返す
// doc.id() を使う場合、自分で永続化をするか、代わりに.snapshotChanges()を使う必要がある
// idをvaluleChanges()で永続化する例は、下記のaddItem()メソッドを参照
this.items = this.itemsCollection.valueChanges();
}
addItem(name: string) {
// Persist a document id
const id = this.afs.createId();
const item: Item = { id, name };
this.itemsCollection.doc(id).set(item);
}
}
snapshotChanges()
メソッド
What is it? - 現在のコレクションの状態です。DocumentChangeAction[]
からなるObservableを返します。
Why would you use it? - データのリストがほしいが、同時に関係するメタデータも保持しておきたい場合に利用します。メタデータは、基底となるDocumentReference
、document id、単一のdocumentの配列indexを含んでいます。documentのidにより、データ操作を簡単にすることができます。 このメソッドは、type
プロパティにより、ngrx、forms、animationsなどのその他のAngularシステムとの統合時に威力を発揮します。 各DocumentChangeAction
のtype
プロパティは、ngrx reducers、form states、animation statesなどに使うことが出来ます。
When would you not use it? - 単一の配列よりより複雑な構造、または発生時に変更するプロセスが必要な場合には、向いていません。配列は、Firestore内の変更と同期されます(リモート・ローカル両方)。
Best practices - .snapshotChanges()
で得たデータを変更する(=transformする)のに、observableオペレーターを使います。テンプレート側にはDocumentChangeAction[]
は返さないようにします(下記参照)。
Example
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import 'firebase/firestore';
export interface Shirt { name: string; price: number; }
export interface ShirtId extends Shirt { id: string; }
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let shirt of shirts | async">
{{ shirt.name }} is {{ shirt.price }}
</li>
</ul>
`
})
export class AppComponent {
private shirtCollection: AngularFirestoreCollection<Shirt>;
shirts: Observable<ShirtId[]>;
constructor(private readonly afs: AngularFirestore) {
this.shirtCollection = afs.collection<Shirt>('shirts');
// .snapshotChanges() は DocumentChangeAction[] を返すが、これにはそれぞれの変更で「何が起こったか」の情報が含まれる
// 各情報を利用したい場合には、mapオペレーターを利用する
this.shirts = this.shirtCollection.snapshotChanges().pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as Shirt;
const id = a.payload.doc.id;
return { id, ...data };
}))
);
}
}
stateChanges()
メソッド
What is it? - DocumentChangeAction[]
として、直近の変更をObservableとして返します。
Why would you use it? - 下記の例では、クエリ発行順にソートされた配列を返しています。stateChanges()
は、クエリ順を同期するのではなく、イベントが発生した順に値を取得します。
Reducerメソッドで独自のデータ構造を構築できるため、ngrxとのインテグレーションに適しています。
When would you not use it? - When you just need a list of data. This is a more advanced usage of AngularFirestore.
Example
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import 'firebase/firestore';
export interface AccountDeposit { description: string; amount: number; }
export interface AccountDepoistId extends AccountDeposit { id: string; }
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let deposit of deposits | async">
{{ deposit.description }} for {{ deposit.amount }}
</li>
</ul>
`
})
export class AppComponent {
private depositCollection: AngularFirestoreCollection<AccountDeposit>;
deposits: Observable<AccountDepositId[]>;
constructor(private readonly afs: AngularFirestore) {
this.depositCollection = afs.collection<AccountDeposit>('deposits');
this.deposits = this.depositCollection.stateChanges(['added']).pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as AccountDeposit;
const id = a.payload.doc.id;
return { id, ...data };
}))
);
}
}
auditTrail()
メソッド
What is it? - DocumentChangeAction[]
として、直近の変更をObservableとして返します。stateChanges()
に似ていますが、それまでのイベントを配列として保持します。
Why would you use it? - このメソッドは、一時的ではないという点を除き、stateChanges()
と同じです。配列内の各変更が発生すると値を収集します。これは、アプリケーションの状態全体を再現する必要があるケースにおいて、ngrxと統合するのに便利です。またデバッグにも使うことが出来ます。afs.collection('items').auditTrail().subscribe(console.log)
と書くだけで、発生したイベントすべてをコンソールで確認できます。
When would you not use it? -データのリストが欲しい場合。auditTrailは、AngularFirestoreのより高度な使用法です。
Example
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import 'firebase/firestore';
export interface AccountLogItem { description: string; amount: number; }
export interface AccountLogItemId extends AccountLogItem { id: string; }
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let log of accountLogs | async">
{{ log.description }} for {{ log.amount }}
</li>
</ul>
`
})
export class AppComponent {
private accountLogCollection: AngularFirestoreCollection<AccountLogItem>;
accountLogs: Observable<AccountLogItemId[]>;
constructor(private readonly afs: AngularFirestore) {
this.accountLogCollection = afs.collection<AccountLogItem>('accountLog');
this.accountLogs = this.accountLogCollection.auditTrail().pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as AccountLogItem;
const id = a.payload.doc.id;
return { id, ...data };
}))
);
}
}
eventを制限する
Firestoreには、added
, removed
, modified
の3つのDocumentChangeType
があります。各ストリーミングメソッドは、デフォルトで3つともリスンしています。一方で、メソッドの第一引数に指定することにより、特定の1つのイベントのみをリスンするようにすることもできます。
Basic example
constructor(private afs: AngularFirestore): {
this.itemsCollection = afs.collection<Item>('items');
this.items = this.itemsCollection.snapshotChanges(['added', 'removed']);
}
Component example
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import 'firebase/firestore';
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor="let item of items | async">
{{ item.name }}
</li>
</ul>
`
})
export class AppComponent {
private itemsCollection: AngularFirestoreCollection<Item>;
items: Observable<Item[]>;
constructor(private afs: AngularFirestore) {
this.itemsCollection = afs.collection<Item>('items');
this.items = this.itemsCollection.valueChanges(['added', 'removed']);
}
}
State based vs. action based
これらのメソッドは、2つのカテゴリに分類されます。
State based メソッドは、collectionの状態を"as-is"で返します。一方で、action based メソッドは、collectionに「何が起きたか」を返します。
例えば、ユーザーがリストの中の3つめのアイテムを更新するとき、State based メソッドでは .valueChanges()
がcollectionの中の3つめのアイテムを更新し、JSON配列を返します。これはどのように This is how your state looks.
collectionにdocumentを追加する
生成されたidをもつ新しいdocumentをcollectionに追加する場合、add()
メソッドを使います。このメソッドは、ジェネリクスで指定された型を利用して、、その型を検証します。
Basic example
constructor(private afs: AngularFirestore): {
const shirtsCollection = afs.collection<Item>('tshirts');
shirtsCollection.add({ name: 'item', price: 10 });
}
個々のdocumentの操作
個々のdocumentを取得、更新、または削除するには、 doc()
メソッドを使用できます。このメソッドは、ストリーミング、更新、削除のメソッドを提供する AngularFirestoreDocument
を返します。