【AWS Amplify ノウハウ】 3. GraphQL Schemaの type に @model を付けなかった時の注意すべきこと

2020.07.31

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

Amplifyシリーズの3つ目の記事です!今回は、GraphQL Schemaの type に @model つけなかった時に、どのようなことが起こるのかを説明し、注意すべきことについてお伝えします。

それでは始めます!:)

そもそも @model をなぜつけないんですか??

Amplifyを初めて使われる方は、主にチュートリアルにやってみながらTypeごとにすべて @model ディレクティブをつけているのでしょう。実はディレクティブそれぞれを正確に理解しないとAmplifyを100%活用することは難しいと思います。(どんな技術でも仕様を正確に確認することが大事ですね!)

なので、今回は @model ディレクティブについて書きたいと思います。

@model ディレクティブをつけた type については AppSync 側から該当する type のデータソースとしての DynamoDB テーブルを登録させ、このデータソース (DynamoDB)にアクセスするためのVTL基盤の GraphQL resolver (Request/Response Mapping Template) を自動的に生成してくれます。

つまり、type に @model を書いておくだけでも、以下のようなメリットがあるのでしょう。

  1. データソース(DynamoDB) を生成し、 AppSync と連携してくれる。
  2. Create/Read/Update/Delete/List オペレーションを動作させるための Query や Mutation と、それぞれが必要とする input 等のタイプを自動で生成してくれる。
  3. 生成された Query や Mutation、タイプフィールドresolver等のための Request/Response Mapping Templatesを自動で生成してくれる。
  4. このように生成された CRUD(L) API を、クライアントソースコード側でもアクセスできる API ライブラリーを自動で生成してくれる。 (amplify codegen)

ですが、こういった良いことをわざと使わないケースがありますでしょうか? @model をつけないなんて!

@connection の導入

はい。@model をつけることによって面倒な作業が非常に多く自動化され、開発者の楽になることはよく分かりました。ですが、普段アプリケーションを開発して、サービス開始までしようとすると、複数の type を使って一緒に扱いするのが一般的かと思います。複数だとしても2~3つを意味するのではないですよね。少なくとも20~30のtype、多ければ 100 以上も、もしくはそれよりも多くの type を使うこともありえるでしょう。

問題をシンプルにするために、下記のような Schema を考えてみましょう。

type Post @model {
  id: ID!
  authorID: ID!
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  addressID: ID!
}

type Address @model {
  id: ID!
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

Post を書く User と、User がサービスを利用中に登録する Address が宣言されています。上記の Schema の通りですと、Post タイプに対してQueryする時、 Userタイプの author を Query できない状況になってしまい、一々クライアントで author 情報をまた Query しなければならない状況になってしまいます。このように処理するのであれば GraphQL を使う意味がないでしょう。なので、下記のように @connection ディレクティブを活用し、author と address に対しても Post タイプの対する Query を投げる際に自動的にデータを取得できるように設定が可能です。

type Post @model {
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  addressID: ID!
  address: Address @connection(fields: ["addressID"])
}

type Address @model {
  id: ID!
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

上記のように Schema を作成することで、getPost(id: String!) または listPost() 等をQueryする際、User タイプの author はもちろん、 適切な depth 設定により Address タイプの address までも取得できます。こうなると、1回のQueryだけで、必要なデータすべてが取得できるのです!

ところが、こんなにいいと思っていた @connection を適切ではない depthと一緒に使ってしまうと、パフォーマンスやコストの問題が生じてしまいます。どういうことかというと、Amplify もそうですが、一般的に GrapgQL クライアントを使うときは、クライアント側で GraphQL APIアクセスコードを直接作成するのではなく、Schemaに基づいた codegen等の機能を活用し、自動で生成されたコードを使います。この時、codegenでどれぐらいの depthまでデータを読んでくるかを予め設定しなければなりません。これが depth の概念と必要性となります。

例えば、 depth を 2 までと設定しておくと、 getPost(id: String!) Queryは下のようにデータを取得します。

{
  "data": {
    "getPost": {
      "title": "...",
      "content": "...",
      ...
      "author": {
        name: "...",
        addressID: "...",
        ...
      }
    }
  }
}

authorまではデータを正しく取得したのですが、authorのaddressに対しては depth が2と設定されましたので取得できませんでした。なので、より深いレベルまで一発でデータを取得するためには depthを増やすべきか!と思われるかもしれませんが、この部分に関しては非常に大事な注意点があります。下記に詳しく説明しておりますので、ご興味のある方はご参考ください :)

つまり、depthをむやみに増やしてはいけないということです!このような場合、考えられるアプローチの中の一つが下で説明する @model ディレクティブがついていない type です。

@model ディレクティブがついていない type

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
}

今回は Address タイプに @model ディレクティブを消し、 User タイプで直接 Addressタイプを活用できるように定義しておきました! User タイプと Address タイプの間の @connection ディレクティブがもう不要になったので、depth を 2 に設定しても getPost(id: String!) Query で address フィールドまで取得できるようになりました。

AWS AppSyncコンソールのQuery メニューで、下のように作成し、上から一つずつ createUser -> createPost -> getPostの順番で実行してみます。

mutation createUser {
  createUser(input: {
    name: "Taewoo",
    address: {
      zipcode: "122-122",
      country: "Korea",
      city: "Busan",
      building: "Busan Building",
      state: "",
      street: "Street 1"
    },
  }) {
    id
  }
}
mutation createPost {
  createPost(input: {
    authorID: "createUser の実行で取得したID",
    content: "hello world",
    title: "first post",
  }) {
    id
  }
}
query getPost {
  getPost(id: "createPost の実行で取得したID") {
    title
    content
    author {
      address {
        zipcode
      }
    }
  }
}

User タイプの中の Address タイプの address フィールドの内部フィールド (言葉が複雑なですね;;) に対してもデータをうまく取得してますね!これがなぜできるのか確認するため、実際にデータソースとして登録された DynamoDBを見てみます。

DynamoDB内部に JSON(Map) タイプで保存されてますね!このように保存されると addressタイプの各フィールド別のResolvingは可能になりますが、Address タイプ(JSON)の特定フィールドで "Filtering(検索条件として活用)" することは不可能になります。実際に amplify push 時に生成された amplify/backend/api/${API名}/build/schema.graphql ファイルの ModelUserFilterInput をみてみても addressフィールドは filtering のオプションとして提供していないことが分かります。

input ModelUserFilterInput {
  id: ModelIDInput
  name: ModelStringInput
  and: [ModelUserFilterInput]
  or: [ModelUserFilterInput]
  not: ModelUserFilterInput
}

すなわち、type に @model を付けないことは、カスタムタイプを DynamoDB の中に JSON 形式として保存することになり、その JSON データをそのまま取得することは可能ですが、JSON データの内部フィールドを検索オプションとして活用することは制約が生じてしまうということです!

この事実をAmplifyのGrapgQLSchemaを設計する際に、予め知っておかないと設計が非常に難しくなったり、開発時に大変になったりするかもしれません。カスタムタイプを @model ディレクティブ無しで活用する時には、このカスタムタイプのフィールドが検索条件で活用する可能性があるかについてもご検討をお願いします!:D

最後に

今回は、type に @model ディレクティブをつけなかった場合に注意すべきことについて書きました。この記事の内容が役に立ちましたら、本シリーズの他の記事もご覧になり、AmplifyのGraphQL Schemaを設計する時に知っておくべきことを予めキャッチアップし、適材適所に活用することはいかがでしょうか?(笑

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