【AWS Amplify ノウハウ】 4. amplify codegen 時に depth 設定に関する注意点

2020.07.31

こんにちは!コンサル部のテウです。

AWS Amplifyシリーズの4番目の記事です!今回は amplify codegen という機能を活用するときに必ず設定しなければならない depth について注意すべきことを説明したいと思います。AWS Amplify だけでなく、GraphQLの一般的な codegen 機能を活用する際の注意すべきことでもありますので、depth設定についてある程度経験と知識がある方は今回の記事はスキップしても構いません。

それでは、始めます!:)

amplify codegen とは?

amplify codegen とは、AWS Amplify が提供する CLI コマンドの一種類で、GraphQL Schemaに定義された内容に基づき、フロントエンドプロジェクトで使えるAPIエンドポイントにアクセスできるライブラリーを作ってくれる機能です。

type Post @model {
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}
 
type User @model {
  id: ID!
  name: String!
  address: Address
}
 
type Address {
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

例えば、上記の Schema で amplify codegen すれば、Angularを基準にすると(私はAngular派ですw)下のようなコードが自動でgenerateされます。

/* tslint:disable */
/* eslint-disable */
//  This file was automatically generated and should not be edited.
import { Injectable } from "@angular/core";
import API, { graphqlOperation } from "@aws-amplify/api";
import { GraphQLResult } from "@aws-amplify/api/lib/types";
import * as Observable from "zen-observable";
 
export type CreatePostInput = {
  id?: string | null;
  authorID: string;
  title: string;
  content: string;
};
 
export type ModelPostConditionInput = {
  authorID?: ModelIDInput | null;
  title?: ModelStringInput | null;
  content?: ModelStringInput | null;
 
...
 
  async UpdateUser(
    input: UpdateUserInput,
    condition?: ModelUserConditionInput
  ): Promise<UpdateUserMutation> {
    const statement = `mutation UpdateUser($input: UpdateUserInput!, $condition: ModelUserConditionInput) {
        updateUser(input: $input, condition: $condition) {
          __typename
          id
          name
          address {
            __typename
            zipcode
            country
            state
            city
            street
            building
            etc
          }
        }
      }`;
    const gqlAPIServiceArguments: any = {
      input
    };
    if (condition) {
      gqlAPIServiceArguments.condition = condition;
    }
    const response = (await API.graphql(
      graphqlOperation(statement, gqlAPIServiceArguments)
    )) as any;
    return <UpdateUserMutation>response.data.updateUser;
  }
 
...
 
OnDeleteUserListener: Observable<OnDeleteUserSubscription> = API.graphql(
    graphqlOperation(
      `subscription OnDeleteUser {
        onDeleteUser {
          __typename
          id
          name
          address {
            __typename
            zipcode
            country
            state
            city
            street
            building
            etc
          }
        }
      }`
    )
  ) as Observable<OnDeleteUserSubscription>;
}

本記事がむやみに長くなることを防ぐために結構省略しましたが、正確には790ラインのコードが生成されました。本番用のアプリケーションでは、Schemaの中のタイプの数が上記の3つだけじゃなく、数十から数百まで増えることを考えれば、数万から数十万ラインまでもコードを自動的に生成してくれる非常に強力な機能です。

このような強力な機能をちゃんと活用するためには depth という項目の設定がとても!!と~~っても!!重要です。

codegen の depth について

codegen は AWS Amplify だけの独自的な機能なわけではありません。GraphQL ecosystem と GraphQL クライアント CLI を提供するツールの中ではすでに codegen という機能は必須機能として位置づけられています。

直接カスタムQueryを作成する場合は該当されませんが、Queryを自動的に生成してくれる際、特に下のように循環参照をしなければならないモデルの場合、必須的にdepthを設定し、無限にQueryのdepthが深くならないようにすることが非常に重要です。

type Post @model
  @key(name: "byUser", fields: ["authorID"])
{
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}
 
type User @model {
  id: ID!
  name: String!
  address: Address
  posts: [Post] @connection(keyName: "byUser", fields: ["id"])
}

上記のようなSchemaの場合、UserタイプとPostタイプがお互いを参照するように構成されているので、下のようにQueryを無限に深く作成することができます。

query InfinitelyDeepQuery {
  getUser(id: "USER_ID") {
    posts {
      items {
        author {
          posts {
            items {
              author {
                ...
              }
            }
          }
        }
      }
    }
  }
}

なので、パフォーマンスの低下やコストが無駄にかかるのを防ぐために、depth は必ず適切に調整しなければなりません。

適切な depth を設定することでクライアント側でのコード作成がより楽になるところはありますが、depth を深くすればするほど、サーバにかかる負荷という点、かつ、特にAmplifyの場合はDynamoDBをデフォルトデータソースとして活用するので、リクエスト数によってコストが高くなる点を、必ず考慮する必要があります。

特定のページのニーズにすべてのQueryを合わせないこと

アプリケーションのコードを作成すると、特定のページでは他のページに比べて非常に深いレベルでのQueryが求められる場合もあります。codegen の depth を該当ページのニーズより浅く設定すると、この特定のページでは、Query を何回に分けて呼び出さないといけないので、コードの複雑度管理やステート管理がさらに難しくなります。例えば、他のページでは depth = 5 に設定することで行けたとしても、特定のページでより深いレベルのdepthが求められたら?

あるECサイトで、下のようなデータが必要なページがあったとします。例として、商品の詳細ページで他のおすすめ商品と、そのおすすめ商品のレビューが見える、ちょっと複雑なページを想像してみましょう。

- product
  - recommendedProducts # おすすめ商品
    - items
      - product
        - name
        - image
        - price
        - reviews # そのおすすめ商品のレビュー
          - items
            - content
            - author # レビューを書いたユーザ
              - profileThumbnailImageURL
              - username

このように考えるだけでも、すでに depth は 8まで増えてしまいます。これが、極端的に複雑に構成したように見えますでしょうか?実際に開発する際には 10 以上の depth が求められるページも時々あると思います。

こんな時にはどうすればいいでしょうか?もちろん、該当のページで depth 5 の Query を何回かに分けて呼び出す方法も良いかもしれませんが、そもそも直接バックエンドを構築せず、あえてAWSAmplifyを選択した理由について改めて考える必要があると思います。

バックエンド開発の負担を軽減し、初期サービスのMVPを迅速に構築するためにAWS Amplifyを導入するケースが一般的であるという前提で、上の状況を見ると、工数を減らすためになるべく複雑なコードを直接作成するよりは、簡単に一発ですべてのデータを取得できる Query を利用した方が当然楽でしょう。つまり、上のシナリオ通りであれば、8 depth レベルの Query をどうしても作成したい、ということです。

では、上記の特定のページのニーズを満たすために codegen の depth レベルを 8 に設定するのか!?

私はそうではないと思います。全体的なパフォーマンスのために codegen の depth は 5 そのままにし、該当のページで必要とするQueryだけを "直接" 作成して使えば良いでしょう。それによって、全体的な depth を浅く維持することができるとともに、特定 Query だけ深い depth の Query を作成することが可能になります。

本記事では、このようにクライアント側でどうやってこういった Query を直接作成できるのかを Angular を基盤にしてご紹介します。非常に簡単ですので、React や Vue 上でも同じく簡単にQuery作成ができます。

Angular で直接 Query を作成する方法

type Blog @model {
  id: ID!
  name: String!
  posts: [Post] @connection(keyName: "byBlog", fields: ["id"])
}
type Post @model @key(name: "byBlog", fields: ["blogID"]) {
  id: ID!
  title: String!
  blogID: ID!
  blog: Blog @connection(fields: ["blogID"])
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}
type Comment @model @key(name: "byPost", fields: ["postID", "content"]) {
  id: ID!
  postID: ID!
  post: Post @connection(fields: ["postID"])
  content: String!
}

上記のようなモデルがあったとします。amplify codegen の depth をデフォルトの 2 に設定し、それ以上の depth レベルが求められるQueryをどうやって直接作成できるのかを見てみます。

今回はAngularの新しいプロジェクトを作り、amplify initをした後に、上のSchemaをamplify pushにてデプロイしておいた前提で、以下の作業を行います。Amplifyの基礎は既にご存知であり、より有効活用したいと思っていらっしゃる方々が本記事のターゲットになりますので、Angularの上にどうやってamplifyのセッティングするかはスキップします。(実は Amplify 公式ドキュメントに丁寧に書かれています笑)

src/app/app.component.ts ファイルにしたのように作成します。

import { Component } from '@angular/core';
import { APIService } from './API.service';
import API, { graphqlOperation } from "@aws-amplify/api";
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor(public apiService: APIService) {}
  ngOnInit() {
    const blogID = "02de1fc1-d07c-420d-b338-54c898e58436"
    this.GetBlogDepthFive(blogID).then((result) => {
      console.log(result);
    })
  }
  async GetBlogDepthFive(id: string): Promise<any> {
    const statement = `query GetBlog($id: ID!) {
        getBlog(id: $id) {
          posts {
            items {
              comments {
                items {
                  content
                }
              }
            }
          }
        }
      }`;
    const gqlAPIServiceArguments: any = {
      id
    };
    const response = (await API.graphql(
      graphqlOperation(statement, gqlAPIServiceArguments)
    )) as any;
    return <any>response.data.getBlog;
  }
}

ここでのポイントは、19行目の getBlog の Query は amplify codegen にて自動的に生成された resolver を使っているということです。なので、カスタムresolverを開発することではなく、単純に既存のresolverを活用し、 depthだけをより深くする形になります。そうすると、こんなに簡単にメソッド一つだけを定義し、より深いdepthレベルのQueryを作成して使うことができます。

実際に上のコードを動作させると、下のような結果になります。

最後に

本記事では、amplify codegen の depth 設定に関する注意事項と、depth を直接カスタマイズした Query を作成する方法について説明しました。codegen のための depth を適切なレベルで管理し、必要に応じて直接 Query を作成することで、全体的なアプリケーションのパフォーマンスやコストまで最適化ができると思います。この記事を読んでいただき、AWS Amplifyの100%活用により一歩近くなれたら、今回のブログは大成功だと思います!:D

以上、コンサル部のテウでした!