GraphQLで正規化されていないデータを整形する　――Resolverの責務について――

普段、GraphQLを用いてAPI開発をしているのですが、**正規化されていないデータベースからレコードを取り出し、それを編集して綺麗なスキーマに載せ替える**ようなAPIを作成することが度々あります。
このようなケースを取り扱う場合、各Resolverにどこまでの責務をもたせるべきかについて、混乱が生じることも多いと感じます。そこで今回は、具体的なケース例を取り上げつつ、自分なりの考え方を整理してみようと思います。

# 前提
データベースはこちら。都道府県、市区町村ごとの住民テーブルですが、正規化されていません。
| prefectureId | prefectureName | cityId | cityName | residentId | residentName |
|:-:|:-:|:-:|:-:|:-:|:-:|
|  p001 | Tokyo  |  c001 | Shinjuku  | r001  |Taro Yamada   |
| p001  |  Tokyo | c001  |  Shinjuku | r002  |  Hanako Suzuki |
|  p001 | Tokyo  | c002 | Shibuya  | r003  | Kenji Tanaka  |
| p002  | Osaka  | c003  |  Umeda | r004  | Mika Sato  |

データ型はこちら。
```typescript:entityModelResident.ts
export interface EntityModelResident {
    residentId: string;
    residentName: string;
    cityId: string;
    cityName: string;
    prefectureId: string;
    prefectureName: string;
}
```

これを次のGraphQLスキーマに載せ替えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254418/7c74ddb2-95ee-b82c-6b99-692532d2164c.png)

スキーマファイルはこちら。
```graphql:schema.gql
type Resident {
  """住民ID"""
  id: String!

  """住民名"""
  name: String!
}

type City {
  """市区町村ID"""
  id: String!

  """市区町村名"""
  name: String!

  """住民"""
  residents: [Resident!]!
}

type Prefecture {
  """都道府県ID"""
  id: String!

  """都道府県名"""
  name: String!

  """市区町村"""
  cities: [City!]!
}

type Query {
  prefectures(userId: String): [Prefecture!]!
}
```

# 設計方針
では、上記の前提をもとに、各Resolverで何をどこまで実装するべきかを検討していきましょう。Resolverは各TypeとQuery/Mutationに紐づいて作成されるため、今回は以下の4つとなります。
| Type | 作成するResolver |
|:-:|:-:|
| Query (prefectures)  | PrefecturesResolver  |
| Prefecture  | PrefectureResolver  |
| City  | CityResolver  |
| Resident  | ResidentResolver  |

Resolver設計のポイントは、**各Resolverが、自分よりも下位のResolverの仕事を奪ってしまわない**ようにすることです。

例えば、極端な設計を考えると、最上位のPrefecturesResolverでPrefecture、City、Residentという3つのノード全てを完成させることもできます。しかしその場合は、
```graphql
query {
  prefectures(userId: "admin"){
    id
    name
  }
}
```
このようなQueryがリクエストされた際に、不要であるはずの処理（例えば「住民単位で元データをグループ化する」処理など）が動いてしまうことになり、パフォーマンスが最適化されません。

したがって、様々なリクエストのパターンに応じて、最適なパフォーマンスを発揮することを重視するのであれば、各Resolverはできるだけ下位のResolverに処理を委譲すべきです。

それを実現するために、各Resolverがやるべきことは
- **担当するTypeの子ノードが、葉ノード（子ノードをもたないノード）であれば、最後まで完成させる**
- **担当するTypeの子ノードが、内部ノード（子ノードをもつノード）であれば、子ノードをキーごとに区切る**

の2つです。逆に言うと内部ノードを完成させるのは責任超過ということになります。

上記のアイデアに基づいて、今回作成するResolverとその責務を以下にまとめました。
| Resolver | 責務 |
|:-:|:-:|
| ResidentResolver  | Residentノードに紐づく葉ノード(id, name)を完成させる。  |
|  CityResolver |  Cityノードに紐づく葉ノード(id, name)を完成させる。住民IDごとに元データ EntityModelResident[] をグループ化する。 |
|  PrefectureResolver | Prefectureノードに紐づく葉ノード(id, name)を完成させる。市区町村IDごとに元データ EntityModelResident[] をグループ化する。  |
| PrefecturesResolver  | 正規化されていないテーブルからデータを取得する。都道府県IDごとに元データ EntityModelResident[] をグループ化する。  |


この考え方は、テーブルが正規化されている場合にも当てはまります。正規化されている場合、各Resolverの責務は次のようになります。
| Resolver | 責務 |
|:-:|:-:|
| ResidentResolver  | Residentノードに紐づく葉ノード(id, name)を完成させる。  |
|  CityResolver |  Cityノードに紐づく葉ノード(id, name)を完成させる。住民テーブルからデータを取得する。 |
|  PrefectureResolver | Prefectureノードに紐づく葉ノード(id, name)を完成させる。市区町村テーブルからデータを取得する。  |
| PrefecturesResolver  | 都道府県テーブルからデータを取得する。  |


正規化されていれば、データを取得した時点で当然「子ノードがキーごとに区切られている」状態になるので、やっていることは同じです。

逆に言うと、**正規化されていない場合も、正規化されている場合と同じタイミングでグループ化すればいいだけ**と考えると、混乱しなくてよいかもしれません。

# 実装のポイント
では、上記の設計方針に基づいて、各Resolverを作成していきます。

まず、PrefecturesResolverを作成します。
Prefecturesの子ノードは、Prefectureです。Prefectureは中間ノードなので、キーであるprefectureId単位でデータをグルーピングしてあげます。
```typescript:prefectures.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { Prefecture } from '../prefecture/models/prefecture.model';
import { EntityModelResident } from './models/entityModelResident';
import { map, Observable, of } from 'rxjs';

@Resolver(() => [Prefecture])
export class PrefecturesResolver {
    @Query(() => [Prefecture])
    public prefectures(): Observable<Prefecture[]> {
        return this.fetchMockData().pipe(
            map((residents) => {
                // 都道府県単位でデータをグループ化
                // key: prefectureId, value: PrefectureのMapを作成
                const prefectureMap = new Map<string, Prefecture>();

                residents.forEach((resident) => {
                    const prefectureId = resident.prefectureId;

                    // 都道府県をマップに追加（初回のみ）
                    if (!prefectureMap.has(prefectureId)) {
                        prefectureMap.set(prefectureId, {
                            id: prefectureId,
                            name: resident.prefectureName,
                            cities: residents.filter((resident) => resident.prefectureId === prefectureId) // ここではcitiesを解決しない（PrefectureResolverの責務）
                        });
                    }
                })

                return Array.from(prefectureMap.values());
            })
        );
    }

    /**
     * Backend APIを呼び出すメソッドのモック
     * @returns 居住する都道府県、市区町村の情報をもった住民の一覧
     */
    private fetchMockData(): Observable<EntityModelResident[]> {
        return of([ 
          //前提に記載したモックデータ 
        ]);
    }
}

```
次に、PrefectureResolverを作成します。
Prefectureの子ノードは、id, name, citiesです。idとnameは葉ノードなので完成させる必要がありますが、parentから渡ってきた同名のプロパティをセットするだけなので、省略可能です。
citiesは中間ノードなので、先ほど同様にキーであるcityId単位でデータをグルーピングしてあげます。

```typescript:prefecture.resolver.ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Prefecture } from './models/prefecture.model';
import { City } from '../city/models/city.model';

@Resolver(() => Prefecture)
export class PrefectureResolver {
    @ResolveField(() => [City])
    public cities(@Parent() parent: Prefecture): City[] {
        const cities = parent.cities;

        // 市区町村単位でデータをグループ化
        // key: cityId, value: CityのMapを作成
        const cityMap = new Map<string, City>();

        cities.forEach((city) => {
            const cityId = city.cityId;

            // 市区町村をマップに追加（初回のみ）
            if (!cityMap.has(cityId)) {
                cityMap.set(cityId, {
                    id: cityId,
                    name: city.cityName,
                    residents: cities.filter((city) => city.cityId === cityId) // ここではresidentsを解決しない（CityResolverの責務）
                });
            }
        })

        return Array.from(cityMap.values());
    }
}

```
ちなみに、idとnameを省略しない場合は、このように書きます。
```typescript:prefecture.resolver.ts
    // 同名のプロパティをセットするだけなので、省略可能
    @ResolveField(() => String, { description: '都道府県ID', nullable: false })
    public id(@Parent() parent: Prefecture): string {
        return parent.id;
    }

    // 同名のプロパティをセットするだけなので、省略可能
    @ResolveField(() => String, { description: '都道府県名', nullable: false })
    public name(@Parent() parent: Prefecture): string {
        return parent.id;
    }
```

次に、CityResolverを作成します。ここまでくれば、こなれたものです。
Cityの子ノードは、id, name, residentsです。idとnameは葉ノードなので完成させる必要がありますが、parentから渡ってきた同名のプロパティをセットするだけなので、省略可能です。
residentsは中間ノードなので、先ほど同様にキーであるresidentId単位でデータをグルーピングしてあげます。

```typescript:city.resolver.ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { City } from './models/city.model';
import { Resident } from '../resident/models/resident.model';

@Resolver(() => City)
export class CityResolver {
    @ResolveField(() => [Resident])
    public residents(@Parent() parent: City): Resident[] {
        const residents = parent.residents

        // 住民単位でデータをグループ化
        // key: residentId, value: ResidentのMapを作成
        const residentMap = new Map<string, Resident>();

        residents.forEach((resident) => {
            const residentId = resident.residentId;

            // 住民をマップに追加（初回のみ）
            if (!residentMap.has(residentId)) {
                residentMap.set(residentId, {
                    id: residentId,
                    name: resident.residentName,
                });
            }
        })

        return Array.from(residentMap.values());
    }
}

```

最後に、ResidentResolverを作成します。
Residentの子ノードは、id, nameです。idとnameは葉ノードなので完成させる必要がありますが、parentから渡ってきた同名のプロパティをセットするだけなので、省略可能です。

```resident.resolver.ts
import { Resolver } from '@nestjs/graphql';
import { Resident } from './models/resident.model';

@Resolver(() => Resident)
export class ResidentResolver { }
```
以上で、完成です。

↓それでは、サーバを起動してリクエストを投げてみましょう。
```graphql
query {
  prefectures{
    id
    name
    cities {
        id
        name
        residents {
            id
            name
        }
    }
  }
}
```
↓すると、きちんとグルーピングされて返ってきました。
```
{
    "data": {
        "prefectures": [
            {
                "id": "p001",
                "name": "p001",
                "cities": [
                    {
                        "id": "c001",
                        "name": "Shinjuku",
                        "residents": [
                            {
                                "id": "r001",
                                "name": "Taro Yamada"
                            },
                            {
                                "id": "r002",
                                "name": "Hanako Suzuki"
                            }
                        ]
                    },
                    {
                        "id": "c002",
                        "name": "Shibuya",
                        "residents": [
                            {
                                "id": "r003",
                                "name": "Kenji Tanaka"
                            }
                        ]
                    }
                ]
            },
            {
                "id": "p002",
                "name": "p002",
                "cities": [
                    {
                        "id": "c003",
                        "name": "Umeda",
                        "residents": [
                            {
                                "id": "r004",
                                "name": "Mika Sato"
                            }
                        ]
                    }
                ]
            }
        ]
    }
}
```
以上、正規化されていないデータベースからレコードを取り出し、それを編集して綺麗なスキーマに載せ替えるケースにおける、各Resolverの責務の考え方でした。

# 実装例
詳細なソースコードを見たい方は、こちらをどうぞ。

https://github.com/pbnakao/backend-bff-tutorial/tree/feature/withoutArgs/04.graphql-insights/bff/src/graphql