AngularFirestoreでreference型のフィールドを再帰的に解決しつつアプリ側のモデルに変換してデータを受け取る
Angular + @angular/fire + Cloud Firestore でデータの取得に苦心したので備忘録です😓
Firestoreのデータにreferenceの配列があるケース
下図のような「投稿( posts
)」と「コメント( comments
)」というコレクションがあります。
posts
のドキュメントは comments
というフィールドを持っていて、これは comments
コレクションのドキュメントへの参照( reference
型)になっています。
このような場合に、投稿を取得すると同時に配下のコメントも全件再帰的に取得して、AngularFirestoreのドキュメントオブジェクトではなくアプリ側で作ったモデルのオブジェクトに変換して受け取りたいというのが今回の要件です。
大前提として、Firestoreの
reference
型は親を取得したら子も芋づる式に取得できたりはしません。クライアント側で参照を辿って取得してあげる必要があります。参考: angular - access data from document referenced inside firestore collection - Stack Overflow
1. モデルのインターフェースを用意
// src/models/post.ts
import { Comment } from './comment';
export interface Post {
title: string,
body: string,
comments: Comment[],
}
export interface Comment {
body: string,
}
2. リポジトリサービスクラスを用意
$ ng g s post-repository
$ ng g s comment-repository
// src/repositories/post-repository.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AngularFirestore } from '@angular/fire/firestore';
import { Post } from '../models/post';
@Injectable({
providedIn: 'root'
})
export class PostRepositoryService {
constructor(
private firestore: AngularFirestore,
) {}
get(id: string): Observable<Post> {
// ここをどう実装するか
}
}
// src/repositories/comment-repository.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AngularFirestore } from '@angular/fire/firestore';
import { Comment } from '../models/comment';
@Injectable({
providedIn: 'root'
})
export class CommentRepositoryService {
constructor(
private firestore: AngularFirestore,
) {}
get(id: string): Observable<Comment> {
// ここをどう実装するか
}
}
こんな感じでバックエンドと会話する責務をリポジトリに閉じ込めて、コンポーネントからはリポジトリの get()
メソッドを使うようにします。
あとはこのリポジトリを実装するだけです。(それが一番難しい)
3. リポジトリサービスクラスを実装
必死でObservable職人になった結果、今回は以下のような実装になりました。
// src/repositories/post-repository.service.ts
import { Injectable } from '@angular/core';
import { Observable, zip } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { AngularFirestore } from '@angular/fire/firestore';
import { Post } from '../models/post';
import { Comment } from '../models/comment';
import { CommentRepositoryService } from './comment-repository.service';
@Injectable({
providedIn: 'root'
})
export class PostRepositoryService {
constructor(
private firestore: AngularFirestore,
private commentRepository: CommentRepositoryService,
) {}
get(id: string): Observable<Post> {
return this.firestore.doc<Post>(`posts/${id}`).valueChanges().pipe(
map((post: Post) => {
return {
post: post,
commentIds: post.comments.map((comment: any) => comment.path.replace(/^comments\//, '')),
};
}),
switchMap(({post, commentIds}) => {
return zip(...commentIds.map(commentId => this.commentRepository.get(commentId))).pipe(
map((comments: Comment[]) => {
return Object.assign(post, {comments: comments}) as Post;
}),
);
})
);
}
}
// src/repositories/comment-repository.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AngularFirestore } from '@angular/fire/firestore';
import { Comment } from '../models/comment';
@Injectable({
providedIn: 'root'
})
export class CommentRepositoryService {
constructor(
private firestore: AngularFirestore,
) {}
get(id: string): Observable<Comment> {
return this.firestore.doc<Comment>(`comments/${id}`).valueChanges();
}
}
CommentRepositoryService
のほうは普通に ドキュメント に書かれているとおりの使い方をしているだけです。
問題は PostRepositoryService
のほうですね。やっていることの流れは、
- 普通に
post
を取得 - この時点では
post.comments
はAngularFirestoreのドキュメントの配列になっているので、それを一旦 ドキュメントIDの配列 に変換 -
post
とcommentIds
をセットにして次のパイプライン(switchMap
)に流す - 受け取った
commentIds
を分解してCommentRepositoryService.get()
に投げる(結果、Observable<Comment>
の配列ができる) - zip を使って「Observableの配列」を「配列のObservable」に変換
-
post.comments
の中身を、取得完了したコメントの配列で上書きして、完了
という感じです。
おまけ
ちなみに上記のように .valueChanges()
でドキュメントをストリームに変換すると、ドキュメントIDなどのメタデータは取得することができず、あくまでデータの内容しか手に入りません。
もしアプリ側でドキュメントIDを持つような実装にしたい場合は、 .valueChanges()
の代わりに .snapshotChanges()
を使って自力でデータを取り出すコードを書けば対応できます。
参考:angularfire2 - How to include the document id in Firestore collection in Angular 5 - Stack Overflow
Discussion