【実践・爆速マスター】Amplify Gen2でGraphQLを操る！驚異のAPI開発術

# はじめに
Amplify大好き@makishy（きしだ）です。またまた煽りタイトルすみません。。。
[前回](https://qiita.com/makishy/items/6072d4e8bebea0f1604c)は、Amplify Gen2を使った認証に関する説明をしました。今回も引き続き同じユースケースを使ったAmplify Gen2の実践的な活用方法について紹介していきたいと思います。

# ユースケース（おさらい）
## イベント予約システム
イベント予約システムには以下の機能が求められているとします。
- システム利用にはユーザーログインが必要
- イベント企画者は、イベントの基本情報（日時、場所、タイトル、詳細説明）を登録できる
- ユーザーは、登録されたイベント確認できる
- ユーザーは、イベントへの申し込みができる
- イベントの申し込み結果をユーザーにメール通知する

## 必要な機能
上記システムでは次の機能を必要としていました。
- アカウント登録
- ログイン
- イベントの登録
- イベントの参照
- イベントの申し込み
- メール通知

アカウント登録、ログインは[前回](https://qiita.com/makishy/items/6072d4e8bebea0f1604c)解説したので、残りのうち、次の機能に着目しようと思います。

- イベントの登録
- イベントの参照
- イベントの申し込み

これらの機能に必要なバックエンドのAPIのデータ設計および登録、参照、更新の処理について学んでいきたいと思います。

# アーキテクチャ概要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/dc01dab3-29cf-4e07-8525-159cbb3a4ce9.png)

# AppSync(GraphQL)の説明
本記事では、バックエンドにAppSyncというサービスを利用します。AppSyncは、API開発を簡素化するマネージドサービスです。GraphQLを活用すると、柔軟でスケーラブル、そしてリアルタイムなAPIを構築することができます。
Amplify Gen2を使うと、直感的なスキーマ定義により簡単かつ柔軟にセキュリティを含む高度な設定を行うことができます。

では、ユースケースに沿って具体的な設定方法に進んでいきましょう。

# スキーマ設計
まず、今回のユースケースで必要となるテーブルの整理をします。

「イベント予約」という行為を分解すると、「イベント」と「ユーザー」とその２つを紐づける「予約」という概念が必要になりそうです。（場所や時間など他の概念が必要になるケースもありますが簡単のため今回はこの３つとします。場所も時間も制限なく使えちゃいます）
エンティティとしては以下の項目を想定します。（エンティティとは、ソフトウェア開発やデータモデリングにおいて非常に重要な概念で、システム内で独立して識別可能な対象や実体を表します。）

- 予約
- ユーザー
- イベント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/79bc09e5-fe13-4cfa-8cf5-08b72458c668.png)

## スキーマ定義
Amplify Gen2では、`amplify/data/resouce.ts`ファイルにスキーマの定義を書きます。Gen2では、schema.graphqlというファイルに定義していたもの同じ考え方ですが、記法が大きく変わっています。
直接的に関係があるかは公式には言及されている記事はないですが、[zod](https://www.npmjs.com/package/zod)というTypescriptの人気のバリデーションライブラリとよく似た書き方をします。

ユーザー、イベント、予約、３つエンティティそれぞれスキーマ定義すると、例えば以下のような実装となります。

```resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  User: a.model({
    id: a.id().required(),
    name: a.string().required(),
    bio: a.string(),
  }).authorization((allow) => [allow.owner(), allow.authenticated().to(['read'])]),

  Event: a.model({
    id: a.id().required(),
    name: a.string().required(),
    capacity: a.integer(),
    startAt: a.datetime().required(),
    endAt: a.datetime().required(),
    description: a.string(),
  }).authorization((allow) => [allow.owner(), allow.authenticated().to(['read'])]),

  Reservation: a.model({
    id: a.id().required(),
    eventDate: a.string().required(),
    userId: a.string().required(),
    status: a.enum(['PENDING', 'CONFIRMED', 'CANCELLED'])
  }).authorization((allow) => [allow.owner(), allow.authenticated().to(['read'])])
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

```
上の例について少し解説をします。

Amlify Gen2ではスキーマを次のように定義します。
`a.model`でDynamoDBに登録可能なテーブルとなります。その中に必要な属性を定義していきいます。
Eventテーブルの例だと、IDと名前とキャパシティ、イベントの開始終了日時、説明を登録するテーブルが作成されます。
```resource.ts
Event: a.model({
    id: a.id().required(),
    name: a.string().required(),
    capacity: a.integer(),
    startAt: a.datetime().required(),
    endAt: a.datetime().required(),
    description: a.string(),
  }).authorization((allow) => [allow.owner(), allow.authenticated().to(['read'])]),
```
`.authorization`の設定によりテーブルに対してアクセス制限を行うことができます。
この例だと、登録したユーザー（オーナー）はすべての操作（登録、更新、参照、削除）、ログイン済みユーザーは参照のみ可能となります。

また、次の箇所で認証モードの設定を行っています。
```resource.ts
export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
```
defaultAuthorizationModeとしてuserPoolを指定していますが、この設定によりCognitoのユーザープールで認証されたユーザーが利用できることになります。

このように簡単なスキーマ定義により、ユーザー応じた柔軟なDB権限の設定が可能となります。この他にも様々な設定方法があるので、スキーマの定義方法についてより詳しく知りたい方は、Amplify Gen2の[公式記事](https://docs.amplify.aws/react/build-a-backend/data/set-up-data/)をご確認ください。

# sandbox作成

以下のコマンドを実行して、自身のサンドボックス環境を作成してみましょう。

```
npx ampx sandbox
```

コマンドが正常終了すると、AWSコンソールのDynamoDB上には次のようにテーブルが作成されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/84479446-a607-4690-b9a2-b56297b7ab24.png)


ここまでで、非常にシンプルなスキーマ定義の方法を学びました。
次に少し、実践的なケースを考えてみます。


# スキーマ拡張
Amplify Gen2でのスキーマ拡張のしやすさについても触れておきたいと思います。
例えば、システムの要件として、ユーザーごとに予約データの全件表示が必要となったとします。
DynamoDBではクエリ効率を考えスキーマ設計を行うのが非常に重要です。

そこで、パーティションキー、ソートキーについて考えます。
DynamoDBにおいて、パーティションキーとソートキーは、テーブルのデータ構造と性能を決定する重要な要素です。これらは合わせて「プライマリキー」を構成し、各項目を一意に識別します。

パーティションキー、ソートキーの説明は以下のとおりです。

## パーティションキー
パーティションキー（別名ハッシュキー）は以下の特徴を持ちます。
- データ分散: DynamoDBはパーティションキーの値に基づいてデータを異なるパーティションに分散させます。
- 高速アクセス: 特定のパーティションキーを持つデータへの直接アクセスが可能で、非常に高速です。
- スケーラビリティ: パーティションキーに基づいてデータが分散されるため、水平方向のスケーリングが容易です。

## ソートキー
ソートキー（別名レンジキー）は以下の特徴を持ちます。
- データの順序付け: 同じパーティションキー内のデータをソートキーの値に基づいて順序付けします。
- 範囲クエリ: 特定のパーティション内で、ソートキーの値の範囲に基づくクエリが可能です。
- 複合キー: パーティションキーとソートキーを組み合わせることで、より細かい粒度でのデータ識別が可能になります。

## パーティションキーとソートキーの組み合わせ
- 一意性: パーティションキーとソートキーの組み合わせは、テーブル内で一意である必要があります。
- クエリ効率: 適切に設計されたパーティションキーとソートキーの組み合わせにより、効率的なデータアクセスパターンを実現できます。
- データモデリング: アプリケーションの要件に基づいて、適切なパーティションキーとソートキーを選択することが重要です。

DynamoDBのDB設計をする際には、低コストとなるよう中心となる代表的なクエリに着目して設計していき、データ構造は特定のビジネス要件に合わせて調整していくのがポイントです。

これを前提に、以下のように予約テーブルのスキーマを見直してみます。

```(抜粋）resource.ts
Reservation: a.model({
    userId: a.string().required(),
    eventDate: a.string().required(),
    eventId: a.string().required(),
    status: a.enum(['PENDING', 'CONFIRMED', 'CANCELLED'])
  })
    .identifier(['userId', 'eventDate'])
    .authorization((allow) => [allow.owner(), allow.authenticated().to(['read'])])
```
`.identifier(['userId', 'eventDate'])`でパーティションキーとソートキーを定義しています。userIdがパーティションキー、eventDateがソートキーです。
詳細については、[公式記事](https://docs.amplify.aws/react/build-a-backend/data/data-modeling/identifiers/#composite-identifier)をご参考ください。セカンダリーインデクスなどさらに高度な設定なども学ぶことができます。


これで、ユーザーごとにeventDataでソートされた予約一覧が表示できるようになります。

改めてsandboxをデプロイすると結果は以下のようにパーティションキー、ソートキーが設定されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/91c402f8-1268-416d-b7a7-891c5dab8ca3.png)

さらに、次のようにAppSync上にapiが作成されていることも確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/04d511a3-c1d8-4b21-b82d-76e06d1b291c.png)

コンソールからクエリ機能を使って、クエリの実行もできます。
（ログインユーザーを作成するmutationの例）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/71af448f-1a2f-4320-98cd-55f7f0d2db07.png)

一方で、今回の例では、`allow.authenticated().to(['read'])`と権限の設定を行っているので、別のユーザーが登録済みのデータに対して更新処理などを行うと以下のように実行エラーとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/dc4f35be-29fa-457a-adc6-5bf82e4e5279.png)


```
"message": "Not Authorized to access updateEvent on type Mutation"
```


# フロントエンド側実装
フロントエンドからは以下のような呼び出し方でデータの取得、登録が行えます。

```appSyncClient.ts
import type { Schema } from "amplify/data/resource"
import { generateClient } from "aws-amplify/api"

const client = generateClient<Schema>()

//　すべてのイベントを取得する
export const listEvents = async () => {
    const events = await client.models.Event.list();
    return events;
}

// 特定のイベントを取得する
export const getEvent = async (id: string) => {
    const event = await client.models.Event.get({id});
    return event;
}

// イベントを作成する
export const createEvent = async (name: string, capacity: number, startAt: string, endAt: string, description: string) => {
    const event = await client.models.Event.create({name, capacity, startAt, endAt, description});
    return event;
}

```

非常に簡単ですね。

ここまでで必要なバックエンドの設計、実装はできました。
次回以降、作成したバックエンドをフロントエンドから呼び出すようにWebアプリケーションを構築してみたいと思います。


# まとめ
Amplify Gen2 を活用することで、イベント予約システムのバックエンドを驚くほどシンプルに構築できることがわかりました。TypeScript ベースの直感的なスキーマ定義を使いながら、GraphQL の柔軟性を活かし、ユーザー認証やデータ管理をスムーズに実装できます。さらに、DynamoDB のパーティションキーやソートキーを適切に設計することで、効率的なデータアクセスも実現しました。

今回の内容を通じて、Amplify Gen2 の強力な機能を活かしつつ、シンプルなコードでスケーラブルなバックエンドを構築できることを実感していただけたのではないでしょうか？次回は、今回作成したバックエンドをフロントエンドと連携し、実際に動作する Web アプリケーションを構築していきたいと思います。

では、引き続き Amplify で快適な開発を楽しんでいきましょう！ 🚀