Vue 3 + vue-apollo 4 で GraphQL mutation を行う(Github の API を利用して issue の登録/更新を行う)

Vue
Vueフロントエンド

Vue 3 + vue-apollo 4 の組み合わせで Mutation を行うための方法を説明します。

環境のセットアップや Query の実行方法については前の記事を参照ください。

前回同様 Github の GraphQL API を利用しようと思いますが、今回は issue の発行や更新で試したいとと思います。実際、GraphQL で Github の issue を操作することはないかもしれませんが、

  • 一覧の表示
  • 一覧の一部分を更新する

という操作自体は、どのアプリケーションでもあるので、使い方さえわかれば応用は聞くと思います。

また、GraphQL (apollo)が初めての場合、キャッシュの理解が重要と感じました。キャッシュするオブジェクトのキーであったり、更新時にバックエンドから返却される戻り値を意識することで、apollo のキャッシュの自動更新の恩恵を受けることが可能になります。
apollo の詳細自体は公式のサイトを参照するとして、ここでは、vue-apollo を利用して、追加や更新といった操作及び、キャッシュを更新する部分まで行っています。

[おさらい]Githubのリポジトリの issue 一覧を取得(Query)

vue-apollo4 を利用した GraphQL 操作自体は前回と同じです。今回は、安直に HelloGraphQL2.vue を用意して、こちらを表示するように変えています。ファイルの中身は次の通りです。

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="result">
    <div>{{result.search.issueCount}}</div>
    <table :border="1" style="border-collapse: collapse">
      <thead>
        <tr>
          <th>No</th>
          <th>number</th>
          <th>タイトル</th>
          <th>内容</th>
          <th>完了済み</th>
          <th>url</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="(v, index) in result.search.nodes"
          :key="v.url"
        >
          <td>{{ index + 1 }}</td>
          <td>{{ v.number }}</td>
          <td>{{ v.title }}</td>
          <td>{{ v.body }}</td>
          <td>{{ v.closed }}</td>
          <td>{{ v.url }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { ref } from "vue";

const { result, loading, error, fetchMore, onResult, onError } = useQuery(
  gql`
    query {
      search(type: ISSUE, query: "repo:<リポジトリ名> is:issue", first: 10) {
        issueCount
        nodes {
          ... on Issue {
            id
            number
            title
            body
            closed
            url
          }
        }
        pageInfo {
            endCursor
            hasNextPage
        }
      }
    }
  `
);

</script>

これを表示すると以下のようになります。<リポジトリ名> は Githubアカウント名 + リポジトリ名です。issue は適当に2つほど事前に作っています。ちゃんと出ていますね。コード的には前回の内容とほぼ同一なのが分かるかと思います。違うのは Github のクエリ部分ですが、? と思われる部分は以下の補足を参照ください。

Github の GraphQL クエリ実行 - 指定リポジトリの issue 取得
Github の GraphQL クエリ実行 – 指定リポジトリの issue 取得

補足:Github の Query

  • 今回 issue を表示させるのに、search を利用していますが、詳細は こちら で確認できます。
    • type :ISSUE や REPOSITORY など、検索対象を指定します
    • query:クエリを指定します。is:xxx は特定の条件にマッチするものだけが指定できます。
  • また「… on Issue」は inline fragments と呼ばれるもので、取り得る型が複数あるときに指定するものです。詳細は こちら です。

[事前準備]対象の Repository の ID を取得する

対象の Repository に issue を登録するには、登録する Repository の ID を知る必要があります(リポジトリ名とは別です)。取得するには次のクエリを実行します。これで取得できる id が Repository ID です。


const { result: resultOfRepoId } = useQuery(
  gql`
    query {
      repository(name: "<リポジトリ>", owner: "<オーナ>") {
        id
        name
      }
    }
  `
);

issue を追加する(mutation)

ここからが mutation です。今回は、画面上で入力したタイトルと本文を入力にして issue を登録させます。元のコードに対して追加した部分は以下になります。

<template>
  <!-- issue のタイトルと本文を入力。 click で mutation を実行させます。 -->
  <input type="text" v-model="newIssueTitle" />
  <input type="text" v-model="newIssueBody" />
  <button @click="addIssue()">登録</button>

  ...以降は同じ
</template>

<script setup lang="ts">
import { useQuery, useMutation } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { ref } from "vue";

const newIssueTitle = ref("");
const newIssueBody = ref("");

// mutation の処理です。構文的には Query とさほど変わりません。
// 注意するのは、パラメータの指定の仕方が Query の場合は第2引数に ref の値そのまま入れれば
// よかったですが、mutation では、関数として指定する必要があります(でないと最新の値で
// mutation がされません。
const { mutate: addIssue } = useMutation(
  gql`
    mutation createIssue($title: String!, $body: String!) {
      createIssue(
        input: { repositoryId: "<事前準備で取得したID>", title: $title, body: $body }
      ) {
        issue {
          id
        }
      }
    }
  `,
  () => ({
    variables: {
      title: newIssueTitle.value,
      body: newIssueBody.value,
    },
  })
);

...以降は同じ
</script>

上記を実行すると、画面上は何も起きませんが、開発者ツールで見るとちゃんとリクエストに成功しています。画面を更新すると、新しい issue が追加できています。

Github の GraphQL ミューテーション実行 - issue の登録
Github の GraphQL ミューテーション実行 – issue の登録

issue 追加後、キャッシュを更新する

issue は登録できましたが、一覧の情報が古いままです。成功後にキャッシュを更新させます。元の Query も少し変えるので、全体を載せます。やっているのは、次の2点です。コード中にもコメントを書いています。

  • mutation の戻り値にキャッシュを更新するために必要な情報を指定する
  • 現時点のキャッシュを取得し、上の情報を差し込み、更新する
<template>
  <input type="text" v-model="newIssueTitle" />
  <input type="text" v-model="newIssueBody" />
  <button @click="addIssue()">登録</button>

  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="result">
    <div>{{ result.search.issueCount }}</div>
    <table :border="1" style="border-collapse: collapse">
      <thead>
        <tr>
          <th>No</th>
          <th>number</th>
          <th>タイトル</th>
          <th>内容</th>
          <th>完了済み</th>
          <th>url</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(v, index) in result.search.nodes" :key="v.url">
          <td>{{ index }}</td>
          <td>{{ v.number }}</td>
          <td>{{ v.title }}</td>
          <td>{{ v.body }}</td>
          <td>{{ v.closed }}</td>
          <td>{{ v.url }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
import { useQuery, useMutation } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { ref } from "vue";

const newIssueTitle = ref("");
const newIssueBody = ref("");

// mutation 後のキャッシュ更新時に、一覧取得のキャッシュを取得するために
// 元のクエリを指定する必要があります。ので、ここではクエリ部分を変数化しています。
const IssueQuery = gql`
  query {
    search(
      type: ISSUE
      query: "repo:<リポジトリ名> is:issue"
      first: 10
    ) {
      issueCount
      nodes {
        ... on Issue {
          id
          number
          title
          body
          closed
          url
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;
const { result, loading, error, fetchMore, onResult, onError } =
  useQuery(IssueQuery);

// mutation の戻り値には、title や body など一覧情報に必要な情報を取得します。
const { mutate: addIssue } = useMutation(
  gql`
    mutation createIssue($title: String!, $body: String!) {
      createIssue(
        input: { repositoryId: "<リポジトリID>", title: $title, body: $body }
      ) {
        issue {
          id
          number
          title
          body
          closed
          url
        }
      }
    }
  `,
  () => ({
    variables: {
      title: newIssueTitle.value,
      body: newIssueBody.value,
    },
    
    // キャッシュを更新します。
    // cache.readQuery で該当するキャッシュが得られます。
    // 第2引数は mutation の戻り値が取得できるので、Query の追加読み込み
    // のときと同様、キャッシュオブジェクトを更新し、cache.writeQuery します。
    // やっていることは、前回の記事のページングのデータのマージと同じです。
    update: (cache, { data: { createIssue } }) => {
      let data = cache.readQuery({ query: IssueQuery });
      data = {
        ...data,
        search: {
          ...data.search,
          nodes: [createIssue.issue, ...data.search.nodes],
        },
      };
      cache.writeQuery({ query: IssueQuery, data });
    },
  })
);

// Repository Id を確認するためのクエリ
// const { result: resultOfRepoId } = useQuery(
//   gql`
//     query {
//       repository(name: "<リポジトリ名>", owner: "<オーナ名>") {
//         id
//         name
//       }
//     }
//   `
// );
</script>

これで、issue を登録すると、追加した issue が一覧に表示されると思います。

登録済みの issue の更新

次はすでにある issue の更新です。GraphQL では上のように新規に追加となったり削除されたものは、自分でキャッシュの更新を行う必要があります。一方で、すでにある情報の更新の場合は、更新リクエストのレスポンスにキャッシュを更新できる情報が返ってくれば、更新処理を書かなくても自動でキャッシュを更新してくれます。便利ですね。詳細は apollo の公式サイト を読むと理解が深まると思います。

以下、追加したところをのせています(サンプルのためコードは簡易です)。

<template>
  ...
  <!-- 一覧で選択した行情報を表示。title, body を変更して更新ボタンを押すと mutation を実行 -->
  <div v-if="selectedIssue">
    <span>更新</span>
    <input type="text" v-model="selectedIssue.title" />
    <input type="text" v-model="selectedIssue.body" />
    <button @click="updateIssue()">更新</button>
  </div>
  ...
       <!-- 各行で、click するとselectedIssueに値をセット -->
       <tr
          v-for="(v, index) in result.search.nodes"
          :key="v.url"
          @click="selectedIssue = { ...v }"
        >
  ...
</template>

<script setup lang="ts">
...

interface SelectedIssue {
  id: string;
  number: number;
  title: string;
  body: string;
  closed: boolean;
  url: string;
}

const selectedIssue = ref<SelectedIssue | null>(null);

...

// 更新用の mutation です。中身は create の場合とほぼ変わっていません。id を指定することになったくらいです
const { mutate: updateIssue } = useMutation(
  gql`
    mutation updateIssue($id: ID!, $title: String!, $body: String!) {
      updateIssue(input: { id: $id, title: $title, body: $body }) {
        issue {
          id
          number
          title
          body
          closed
          url
        }
      }
    }
  `,
  () => ({
    variables: {
      id: selectedIssue.value?.id,
      title: selectedIssue.value?.title,
      body: selectedIssue.value?.body,
    },
  }) // ここで create のときにあった更新処理は不要です
);

画像だとわかりにくいですが、No0(先頭行)をクリックすると 更新の入力ができるようになり、変更して更新ボタンを押すと、一覧上の表記も変更した内容に変わります。GraphQL(というよりは apollo ですかね)、とても簡潔に API 取得だけでなくデータの更新もできます。

Github の GraphQL ミューテーション実行 - issue の更新
Github の GraphQL ミューテーション実行 – issue の更新

読み込みとエラー

Queryと同様、読み込み中(loading), エラー(error) なども同じ方法で参照、利用できます。

const { mutate: addIssue, loading: addIssueLoading, error: addIssueError, onDone } = useMutation(...)

GraphQL にはまだ Subscription がありますが、それはまた別途まとめられたらと思います。

コメント