Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What is going on with this article?
@minato-naka

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

はじめに

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

① 概要説明編
② Amplify利用準備・初期設定編
③ Authでユーザー登録、ログイン機能実装編
④ API(Graphql)でCRUD実装編
↑↑↑今ここ↑↑↑
⑤ StorageでS3に画像保存実装編
⑥ Hostingでアプリ公開&自動デプロイ実装編

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

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

このAmplifyAPI導入でも
Amplify利用準備・初期設定編のAmplify機能の利用手順で説明した通り、
①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 APIAmazon Cognito User Pool
? Do you want to configure advanced settings for the GraphQL APINo, 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)

質問に答えて処理が完了すると、
バックエンド設定ファイルがいくつか作成されます。
コミット

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

いまはサンプルのTodoテーブルが定義されている状態なのでそれは削除し、
AlbumテーブルとPhotoテーブルの定義を自分で記述します。
テーブル定義は、①概要説明編の構築するアプリ仕様で見せたER図を参考にします。
ただし、この時点ではPhotoテーブルのs3keyカラムは作成しません。
このカラムは次の記事のStorage編でS3への画像登録機能を実装する際に追加します。

/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"])
}

コミット

この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 APIYes
? Choose the code generation language targetjavascript
? Enter the file name pattern of graphql queries, mutations and subscriptionssrc\graphql\**\*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptionsYes
? Enter maximum statement depth [increase from default if your schema is deeply nested]2

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

また、今回のamplify pushの後に、
ローカルに src/graphql/queries.jssrc/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一覧

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>

コミット

・Album登録

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>

コミット

・Album更新

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>

コミット

・Album詳細

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>

コミット

・Photo登録

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>

コミット

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

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>

コミット

これで、一通りの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実行しています。

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に画像保存実装編

1
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
minato-naka
アジアクエスト株式会社(福岡オフィス) PHP/Laravel/Rails/AWS/Vue.js/Docker
asia-quest
DX実現を目指す企業と並走する「デジタルインテグレーター」です。 通常のシステムインテグレーションだけではなく、お客様のDXを共に考えるコンサルティングから、 DXに必要な様々なデジタルテクノロジーの専門チームを有し、お客様のゴールに向けてシステムの設計、開発、運用までを一貫して請け負います。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
1
Help us understand the problem. What is going on with this article?