【Angular x Firestore】フィールドの値を元に別のドキュメントを合成して取得する

# はじめに

今回は画像のようなデータベースを例に、RxJSを用いて**別コレクションにあるユーザーのデータを合成しながら、記事のデータを取得する**例を紹介します。

具体的には、**「articlesコレクション内のauthorId」と「usersコレクション内のuserId」**が一致するドキュメントを合成します。

<img width="923" alt="SS 2020-03-17 17.56.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349873/d8340443-b4aa-850c-88c1-12683ee7035a.png">

# サンプルコード

大まかな流れはこのようになっています。

1. articleコレクションを取得
2. usersコレクションを取得
3. ドキュメントを合成

```ts
getArticles(): Observable<ArticleWithUser[]>  {
    // 記事のデータを保管しておく空配列を用意
    let articles: Article[];
    return this.db.collection<Article>(`articles`).valueChanges().pipe(
      // 流れて来たarticlesコレクションをswitchMapでusersコレクションに差し替える
      switchMap((docs: Article[]) => {
        // 差し替えたらarticlesコレクションのデータは取得出来なくなってしまうので保管する
        articles = docs;
        if (articles.length) {
          // filterを使ってauthorIdの重複を無くす
          const authorIds: string[] = articles.filter((article, index, self) => {
            return self.findIndex(item => article.authorId === item.authorId) === index;
          }).map(article => article.authorId);

          // 重複なしのIDリストを使ってusersコレクション内のドキュメントを取得
          // ドキュメントが流れるObserbableが複数ある状態なので、combineLatestで1つにまとめる
          return combineLatest(authorIds.map(userId => {
            return this.db.doc<User>(`users/${userId}`).valueChanges();
          }));
        } else {
          // switchMapはObserbableを返す必要があるのでofを使う
          return of([]);
        }
      }),
      // 流れてきたuserのドキュメントと、保管しておいた記事のドキュメントを合成する
      map((users: User[]) => {
        return articles.map(article => {
          const result: ArticleWithUser = {
            ...article,
            author: users.find(user => user.id === article.authorId),
          };
          return result;
        });
      })
    );
  }
```

> **このコードで登場するRxJSのOperatorsを簡易解説**
> 
> switchMap: Observableを別のObservableに差し替える
> 
> combineLatest: 複数のObservableを1つにまとめる
>
> map: 流れて来た値に何かしら変更を加えて戻す
> 
> 知らないOperatorsはこのサイトで調べると良いです
> https://www.learnrxjs.io/learn-rxjs/operators

### switchMap内でauthorIdの重複をfilterで無くしている理由
**無駄なリクエストを無くすため**です。
重複したままだと、取得した記事のうち100件が同じユーザーの記事だった場合に、99回も無駄にユーザーデータをリクエストすることになってしまいます。

# 複数のメソッドで使い回す
Firestoreでは、このようにコレクションの絞り込みや、並び替えが出来ます。

```ts
return this.db.collection<Article>(`articles`, ref => {
      return ref.where('条件').orderBy('条件').limit('数');
}).valueChanges();
```

実際に記事を取得するシーンでは**「いいね数が多い順で取得」「特定のユーザーの記事だけ取得」**など、条件付きで取得することが多いと思います。

そのような場合**「条件だけが違い、中身はほとんど上記のgetArticlesメソッドと同じ」**というメソッドを量産してしまうのは、[DRY原則](https://xn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/DRY%E5%8E%9F%E5%89%87/)からして良いコードとは言えません。

## 解決方法

個別のメソッドでは**条件指定だけ**を行いましょう。
並び替えや絞り込みが完了したコレクションをgetArticlesに渡します。

ここでは、favoriteの数を参照して降順で並び替えた上で6件だけ取得しています。

```ts
getPopularArticles(): Observable<ArticleWithUser[]> {
    const sorted = this.db.collection<ArticleWithUser>(`articles`, ref => {
      return ref.orderBy('favorite', 'desc').limit(6);
    });
    return this.getArticles(sorted);
  }
```
getArticlesはコレクションを受け取るように変えてあげましょう。
受け取ったsortedを使って、記事とユーザーのデータを合成して返します。

```ts
getArticles(sorted: AngularFirestoreCollection<Article>): Observable<ArticleWithUser[]> {
    let articles: Article[];
    return sorted.valueChanges().pipe(
      switchMap((docs: Article[]) => {
        articles = docs;
// ...以下省略
```
#参考サイト
**CAMP**
https://to.camp/about

Angular × Firebaseを用いたSPA開発を教えているオンラインスクールです。
日本語の情報リソースが少ないAngularの解説動画や記事が載っているため、重宝しています。