【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】④ API（Graphql）でCRUD実装編

# はじめに
この記事は、全部で6記事にわたるたチュートリアルの4つ目です。

[① 概要説明編](https://qiita.com/minato-naka/items/7b445bcf0794189e65a0)
[② Amplify利用準備・初期設定編](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66)
[③ Authでユーザー登録、ログイン機能実装編](https://qiita.com/minato-naka/items/4e4dcfea2bf5231f6c86)
[④ API（Graphql）でCRUD実装編](https://qiita.com/minato-naka/items/03da3987d5a81dfe865f)
↑↑↑今ここ↑↑↑
[⑤ StorageでS3に画像保存実装編](https://qiita.com/minato-naka/items/abffc5e1e949a6fea494)
[⑥ Hostingでアプリ公開＆自動デプロイ実装編](https://qiita.com/minato-naka/items/3123950aa852ee3250fe)


前回の記事では、AmplifyのAuth機能を利用して、
サインアップ、サインイン、サインアウト、パスワードリセットなど
一通りの認証機能を実装しました。

今回は、AmplifyのAPI機能を利用してCRUD機能を一通り実装していきます。
CRUD機能とは、アルバムの表示、登録、更新、削除の一通りの機能のことです。


このAmplifyAPI導入でも
Amplify利用準備・初期設定編の[Amplify機能の利用手順](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66#amplify%E6%A9%9F%E8%83%BD%E3%81%AE%E5%88%A9%E7%94%A8%E6%89%8B%E9%A0%86)で説明した通り、
①AmplifyCLIのaddでバックエンド設定追加
②AmplifyCLIのpushでバックエンドリソース作成
③フロントエンドにバックエンド連携コード実装
の3ステップでAPI機能を追加していきます。

# AmplifyCLIのaddでバックエンド設定追加
ターミナルで下記コマンドを実行します。
`amplify add api`

すると質問がいろいろ表示されるので
下記のように回答しましょう。

`? Please select from one of the below mentioned services: `→`GraphQL`
`? Provide API name: `→`amplifyvuealbum`
`? Choose the default authorization type for the API `→`Amazon Cognito User Pool`
`? Do you want to configure advanced settings for the GraphQL API `→`No, I am done.`
`? Do you have an annotated GraphQL schema? `→`No`
`? Choose a schema template: `→`Single object with fields (e.g., “Todo” with ID, name, description)`

質問に答えて処理が完了すると、
バックエンド設定ファイルがいくつか作成されます。
[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/db54bc7fb741e2a4072b2a287f3b5958cb35e17f)

これらの設定ファイルのうち、重要なのが
`schema.graphql` です。
これは、テーブル定義書のようなもので、
どのテーブルをDBに作るのか、
そのテーブルの持つカラムとその型をどうするか、
テーブル同士のリレーションをどうするか、
などを定義します。

いまはサンプルのTodoテーブルが定義されている状態なのでそれは削除し、
AlbumテーブルとPhotoテーブルの定義を自分で記述します。
テーブル定義は、①概要説明編の[構築するアプリ仕様](https://qiita.com/minato-naka/items/7b445bcf0794189e65a0#%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%E3%82%A2%E3%83%97%E3%83%AA%E4%BB%95%E6%A7%98)で見せたER図を参考にします。
ただし、この時点ではPhotoテーブルのs3keyカラムは作成しません。
このカラムは次の記事のStorage編でS3への画像登録機能を実装する際に追加します。

```text:/amplify/backend/api/amplifyvuealbum/schema.graphql
type Album 
  @model 
  @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String!
  photos: [Photo] @connection(keyName: "byAlbum", fields: ["id"])
}

type Photo
  @model
  @auth(rules: [{ allow: owner }])
  @key(name: "byAlbum", fields: ["albumID"]) {
  id: ID!
  name: String!
  albumID: ID!
  album: Album @connection(fields: ["albumID"])
}
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/cbfdf8fb91b77e65e1bf3df2204b0ee5bbbd8e6d)

このschema.grapqlの記述によって
色々な機能を実現することができますが、
今回は基本機能のみで、詳細の解説は省きます。

基本の記述ルールはこんな感じです。
・typeの後にテーブル名を記述する
・その配下に「カラム名:型」の形式でカラム定義を記述する
・@で始まる「ディレクティブ」という記述で、様々な機能を付与する
・@modelと書くとDynamoDBにテーブルが作成される
・@authと書くとそのテーブルのデータを参照可能なユーザを制限することができる（今回はデータを登録した本人のみそのデータを参照できる設定）
・@connectionと書くとテーブル間のリレーションを定義することができる

他にも様々なディレクティブがあり、いろいろと便利な機能が提供されています。
https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js#using-graphql-transformers

# AmplifyCLIのpushでバックエンドリソース作成

これでAPIのバックエンド設定は作成完了なので
実際にAPIをAWS上に作成します。

下記コマンドを実行します。
`amplify push`

質問がいろいろ表示されるので
下記のように回答します。

`? Are you sure you want to continue? `→`Yes`
`? Do you want to generate code for your newly created GraphQL API `→`Yes`
`? Choose the code generation language target `→`javascript`
`? Enter the file name pattern of graphql queries, mutations and subscriptions `→`src\graphql\**\*.js`
`? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions `→`Yes`
`? Enter maximum statement depth [increase from default if your schema is deeply nested] `→`2`

しばらく待つと処理が完了し、AWS上に実際にリソースが作成されます。
今回はAppSyncやDynamoDBなどのリソースが作成されているので、
実際にAWS上で確認してみましょう。

また、今回のamplify pushの後に、
ローカルに `src/graphql/queries.js` や `src/graphql/mutations.js` 
などのファイルが生成されています。

これらはフロントエンドからAPIにリクエストするための
メソッドリストのようなものです。

Album作成をしたければmutations.jsにある `createAlbum` を実行すればいいし、
Albumの一覧データを取得したければqueries.jsにある `listAlbums` を実行すればいいだけです。

今回のチュートリアルではこれらの記述内容は気にすることなく
デフォルトのままで進めることが可能です。

より応用的なアプリを作成する際には
これらのファイルの内容をカスタマイズすることになります。

このqueryやmutationは、Amplify独自の仕様ではなく、
Graphql自体の共通の仕様です。
詳しく知りたい方は、AmplifyではなくGraphqlの仕様を調べてみてください。

# フロントエンドにバックエンド連携コード実装

バックエンドにAPIを用意できましたので、
そのAPIにリクエストして実際にデータを登録、取得するコードを書いていきます。

・Album一覧
・Album登録
・Album更新
・Album詳細
・Photo登録
まで一気に行きます。
（Photo登録に関しては、この時点では名前だけ登録し、画像自体の登録は次の記事のStorage機能で実装します）


・Album一覧

```vuejs:src/views/album/Index.vue
<template>
  <div>
    <h1>アルバム一覧</h1>
    <router-link custom v-slot="{ navigate }" :to="{ name: 'AlbumCreate' }">
      <button @click="navigate">Add Album</button>
    </router-link>
    <table border="1">
      <tr v-for="(album, index) in albums" :key="album.id">
        <td>{{ album.name }}</td>
        <td>
          <router-link
            custom
            v-slot="{ navigate }"
            :to="{ name: 'AlbumShow', params: { albumId: album.id } }"
          >
            <button @click="navigate">Show Album</button>
          </router-link>
        </td>
        <td>
          <router-link
            custom
            v-slot="{ navigate }"
            :to="{ name: 'AlbumEdit', params: { albumId: album.id } }"
          >
            <button @click="navigate">Edit Album</button>
          </router-link>
        </td>
        <td>
          <button @click="deleteAlbum(index, album.id)">
            Delete Album
          </button>
        </td>
      </tr>
    </table>
  </div>
</template>

<script>
import { API } from "aws-amplify";
import { listAlbums } from "../../graphql/queries";
import { deleteAlbum } from "../../graphql/mutations";

export default {
  name: "AlbumIndex",
  async created() {
    this.getAlbums();
  },
  data() {
    return {
      albums: [],
    };
  },
  methods: {
    async getAlbums() {
      await API.graphql({
        query: listAlbums,
      })
        .then((result) => {
          console.log(result);
          this.albums = result.data.listAlbums.items;
        })
        .catch((error) => {
          console.log(error);
        });
    },
    async deleteAlbum(index, albumId) {
      if (!confirm("Delete Album?")) return;

      await API.graphql({
        query: deleteAlbum,
        variables: { input: { id: albumId } },
      })
        .then((result) => {
          console.log(result);
          this.albums.splice(index, 1);
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/73c0d47c15c2865e74d804b5e2dc87cf8d90f2b4)

・Album登録

```vuejs:src/views/album/Create.vue
<template>
  <div>
    <h1>アルバム登録</h1>
    <form @submit.prevent="submitCreate">
      <label>Name</label><br />
      <input v-model="form.name" placeholder="Enter album name" /><br />
      <input type="submit" value="Submit" />
    </form>
  </div>
</template>

<script>
import { API } from "aws-amplify";
import { createAlbum } from "../../graphql/mutations";

export default {
  name: "AlbumCreate",
  data() {
    return {
      form: {
        name: "",
      },
    };
  },
  methods: {
    async submitCreate() {
      await API.graphql({
        query: createAlbum,
        variables: { input: this.form },
      })
        .then((result) => {
          console.log(result);
          this.$router.push({ name: "AlbumIndex" });
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/23b23ee1cc8d46e1a939f94f1774c5a2a7624e4b)

・Album更新

```vuejs:src/views/album/Edit.vue
<template>
  <div>
    <h1>アルバム編集</h1>
    <form @submit.prevent="submitUpdate">
      <label>Name</label><br />
      <input v-model="form.name" placeholder="Enter album name" /><br />
      <input type="submit" value="Submit" />
    </form>
  </div>
</template>

<script>
import { API } from "aws-amplify";
import { getAlbum } from "../../graphql/queries";
import { updateAlbum } from "../../graphql/mutations";

export default {
  name: "AlbumEdit",
  props: {
    albumId: String,
  },
  async created() {
    this.getAlbum();
  },
  data() {
    return {
      form: {
        id: "",
        name: "",
      },
    };
  },
  methods: {
    async getAlbum() {
      await API.graphql({
        query: getAlbum,
        variables: { id: this.albumId },
      })
        .then((result) => {
          console.log(result);
          this.form.id = result.data.getAlbum.id;
          this.form.name = result.data.getAlbum.name;
        })
        .catch((error) => {
          console.log(error);
        });
    },
    async submitUpdate() {
      await API.graphql({
        query: updateAlbum,
        variables: { input: this.form },
      })
        .then((result) => {
          console.log(result);
          this.$router.push({ name: "AlbumIndex" });
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/c955a46093809ade04dfc6b4fbbdf253c4bce2c5)

・Album詳細

```vuejs:src/views/album/Show.vue
<template>
  <div>
    <h1>アルバム詳細</h1>
    <h2>{{ album.name }}</h2>
    <router-link
      custom
      v-slot="{ navigate }"
      :to="{ name: 'PhotoCreate', params: { albumId: albumId } }"
    >
      <button @click="navigate">Add Photo</button>
    </router-link>
    <table border="1">
      <tr v-for="(photo, index) in album.photos.items" :key="photo.id">
        <td>{{ photo.name }}</td>
        <td>
          ここに画像表示
        </td>
        <td>
          <button @click="deletePhoto(index, photo)">Delete Photo</button>
        </td>
      </tr>
    </table>
  </div>
</template>

<script>
import { API } from "aws-amplify";
import { getAlbum } from "../../graphql/queries";
import { deletePhoto } from "../../graphql/mutations";

export default {
  name: "AlbumShow",
  props: {
    albumId: String,
  },
  async created() {
    this.getAlbum();
  },
  data() {
    return {
      album: {
        name: null,
        photos: []
      },
    };
  },
  methods: {
    async getAlbum() {
      await API.graphql({
        query: getAlbum,
        variables: { id: this.albumId },
      })
        .then((result) => {
          console.log(result);
          this.album = result.data.getAlbum;
        })
        .catch((error) => {
          console.log(error);
        });
    },
    async deletePhoto(index, photo) {
      if (!confirm("Delete Photo?")) return;

      await API.graphql({
        query: deletePhoto,
        variables: { input: { id: photo.id } },
      })
        .then((result) => {
          console.log(result);
          this.album.photos.items.splice(index, 1);
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/24e8d683629fcbf53af98b34bc65f0f4f262b22f)

・Photo登録

```vuejs:src/views/photo/Create.vue
<template>
  <div>
    <h1>写真登録</h1>
    <form @submit.prevent="submitCreate">
      <label>Name</label><br />
      <input v-model="form.name" placeholder="Enter photo name" /><br />
      <p>ここに画像登録フォーム</p>
      <input type="submit" value="Submit" />
    </form>
  </div>
</template>

<script>
import { API } from "aws-amplify";
import { createPhoto } from "../../graphql/mutations";

export default {
  name: "PhotoCreate",
  props: {
    albumId: String,
  },
  data() {
    return {
      form: {
        albumID: this.albumId,
        name: "",
      },
    };
  },
  methods: {
    async submitCreate() {
      await API.graphql({
        query: createPhoto,
        variables: { input: this.form },
      })
        .then((result) => {
          console.log(result);
          this.$router.push({
            name: "AlbumShow",
            params: { albumId: this.albumId },
          });
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/7d5b25d2e59d76c6cf14c0e60dbd94615b4b748f)

ついでに、下層のページに行ったときに上層ページに戻る方法がないので、
画面上部にTOPボタンを追加しておきます。

```diff:src/App.vue
  <div v-if="isSignedIn">
    <amplify-greetings  :username="this.$store.state.user.username"></amplify-greetings>
+   <router-link custom v-slot="{ navigate }" :to="{ name: 'AlbumIndex' }">
+     <button @click="navigate">TOP</button>
+   </router-link>
  </div>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/024e9da7d16e0ba1654f4a0e7afc51d92388bcf6)


これで、一通りのCRUD機能は実装完了です。
各ページのコードの詳しい解説は省略しますが、
ポイントだけ簡単に解説します。

VueでAmplifyのAPIを利用するためには下記3つが必要です。
①APIモジュールをインポートする
②利用するqueryをインポートする
③APIモジュールにqueryと必要なパラメータを渡して実行

シンプルな例として `src/views/album/Create.vue` を見てみます。

> ①APIモジュールをインポートする

こちらのコードです。
`import { API } from "aws-amplify";` 

> ②利用するqueryをインポートする

こちらのコードです。
`import { createAlbum } from "../../graphql/mutations";`
このページでは、アルバム登録をするので `createAlbum` をインポートしていますが、
アルバム一覧を取得したければ `listAlbums` をインポートし、
アルバム削除をしたければ `deleteAlbum` をインポートする、という要領です。

> ③APIモジュールにqueryと必要なパラメータを渡して実行

methodsにある `submitCreate()` にあるこちらのコードでAPI実行しています。

```vuejs
API.graphql({
  query: createAlbum,
  variables: { input: this.form },
})
```

先ほどインポートしたAPIモジュールの `graphql` メソッドに、
・利用するquery（先ほどインポートしたもの）
・必要なパラメータ
を引数として渡すだけです。

パラメータで渡している `this.form` には、画面で入力されたフォーム内容が入っています。

このAPI実行コードの基本形は、
一覧取得、登録、更新、削除などで同じです。

あとはこのAPI実行コードを画面ごとに適切なタイミングで実行するだけです。
アルバム一覧ページでは、画面表示時に `created()` で一覧取得APIを実行します。
アルバム登録、更新ページでは、フォームの送信ボタンクリック時に登録、更新APIを実行します。

今回追加した各ページのコードは、
上記のAPI実行箇所以外は全てVueの初歩的な記述しか使っていないので、
コードを読んでもらえば理解は難しくないと思います。

画像登録機能以外は一通り動く状態になっているので、
各画面で実際にデータ登録して表示を確認してみましょう。
※画像をS3に登録するのは次のStorage機能のところで実装していきます

このチュートリアルアプリでは、バリデーション処理などを入れていないので、
フォームが空欄のままだったりしてもデータ登録できてしまいます。
この辺は、各自で最後に機能追加してみてください。
※空のデータを登録すると、データ表示のページの方でエラーが出てしまったりもするので注意してください

各APIのレスポンスをconsole.logで出力しているので
ChromeデベロッパーツールのConsoleタブで内容を確認してみてください。
アルバム一覧など、取得したデータ内容が確認できると思います。

# おわりに

AmplifyのAPI機能を使って基本のCRUDを一通り実装しました。
今回は一番シンプルで簡単な機能だけを利用しましたが、
AmplifyのAPIでは
・Elasticsearchと連携させて高機能な検索
・Lambdaと連携させてAPIに複雑なロジックを実装する
など、たくさん便利な機能を組み込むことが可能です。

これらの応用機能もそのうち別の記事で解説したいと思います。

次の記事では、StorageでS3に画像保存する機能を実装していきます。
（次に進む前に、LGTMしてもらえるとうれしいです）
[【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】⑤ StorageでS3に画像保存実装編](https://qiita.com/minato-naka/items/abffc5e1e949a6fea494)