⛰️

ついにGraphQLに入門した!

2023/09/24に公開

今さらですが!GraphQL、ついに、挑戦しました👏
興味はあったものの、まだ着手できていなかったのですが、お仕事の関係もあり、挑戦するに至りました。

ということで!GraphQLとは何なのかから、どう実装したのかまでを整理しておこうと思い、本記事を作成しました!
GraphQLを使ったことなくても、「へー、大体こんな感じなんだなー🤔」みたいな感じで伝われば良いなと思っています。

挑戦したこと

今回挑戦したのは、GraphQLです!
「GraphQLってどうやって使うの?」っていうことを学ぶところから、実際に自分で使ってみる、サービスを軽く実装してみるっていうところに挑戦しました。

ちなみにこんなの実装してみました。

みんな大好き、タスク管理ですね👏

GraphQLって何?

まず、GraphQL とは何かについて、整理していきます。

概要

A query language for your API

公式サイトを確認すると、Web APIのための「クエリ言語」と表現されています。

「クエリ言語」と聞くとSQLを使うようなDB操作をイメージするかもしれませんが、GraphQLは関数の実行を通して、データの取得や操作などのリクエストを行うことができます。

特徴としては、以下が挙げられるかと思います。

  • 単一エンドポイントであること。
  • 必要なデータのみを取得することができること。

また、GraphQLは、TypeScriptだけでなく、PythonやRuby、Javaなど複数の言語で使用できます。
GraphQL公式のCode using GraphQLで、サポートされている言語など確認することができます。

基本的な用語

GraphQLを使用するにあたって、基本的な用語を簡単にまとめておきます。

スキーマ

スキーマでは、データの型を定義します。
GraphQLでは、スキーマ定義言語(SDL)で記述します。

使える型は、以下です。

  • スカラ型
    プリミティブな値を表します。
    Int型String型Boolean型などがあります。

  • Non-Null型
    Nullを許容しないことを表します。
    型名の後に!を付けます。

例)

name: String!
  • オブジェクト型
    オブジェクトを表します。フィールドがあり、そのフィールドそれぞれ自体にも型を指定します。
    オペレーションの型もオブジェクト型です。

例1)Int型idフィールドとString型nameフィールドを持つ、オブジェクト型User

type User {
  id: Int
  name: String
}

例2)String型の値を返すQueryhelloを持つ、オペレーションQueryの型。

type Query {
  hello: String
}
  • インプット型
    オブジェクト型のフィールドの引数としてオブジェクトを渡す場合に使えます。
    inputキーワードを使って定義します。

例)

type User {
  id: String
  name: String
}

input InputUserInfo {
  id: String
  name: String
}

type Mutation {
  addUser(userInfo: InputUserInfo) {
    id
    name
  }
}
  • リスト型
    ある型の値を要素として持つ配列を表します。

例)オブジェクト型Userの値を要素として持つ配列を返す、Queryusers

type User {
  id: Int
  name: String
}

type Query {
  users: [User]
}
  • 列挙型
    特定の値に限定することができます。
    enumキーワードを使って定義します。

例)

enum Order {
  ASC
  DESC
}
  • ユニオン型
    複数の型のうち、どれかの型であることを表します。

例)idフィールドの型は、String型Int型のどちらかであること。

type User {
  id: String | Int
  name: String
}
  • インターフェース
    複数の型が共通して持つフィールドを定義します。

リゾルバ

クエリの処理の中身を定義します。どういう処理を行うのかを定義します。

オペレーション

GraphQLでは、リクエストのことを「オペレーション」と呼びます。
QueryMutationSubscriptionの3つがあります。

Query

Queryとは、データの取得を行うときに使うオペレーションです。

query records {
  records {
      name
  }
}
Mutation

Mutationとは、データの作成や編集・削除などのような、データを変更するときに使うオペレーションです。

mutation addRecord($id: ID) {
  addRecord(id: $id) {
      name
      comment
  }
}
Subscription

Subscriptionとは、データの変化があった場合にクライアントへ通知を行い、最新のデータを取得できるようにするオペレーションです。

subscription subscribeRecord {
  addedRecord {
    name
    comment
  }
}

基本編!

それでは、基本的な使い方を見ていきます。

準備する

まずは、必要なパッケージをインストールするなど実装で必要な準備をしていきます。

インストール

まずは、必要なパッケージを入れるところから始めます。

とりあえず、ExpressとGraphQLまわりから。

npm install express express-graphql graphql@15.8.0

TypeScriptを使っているためts-nodeも入れておきましょう。
そのほか、nodemonも入れておくと、コードを変更するたびに起動し直す必要がなくなるので便利です。

index.tsを用意する

index.tsを用意しておきましょう。

index.ts
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { GraphQLSchema, buildSchema } from 'graphql';
const app: express.Express = express();
const port: number = 3000;

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

起動スクリプトを用意する

起動するとき、nodemonts-nodeを打つのも面倒なので、package.jsonに"start"という起動スクリプトを追加してしまいます。

package.json
"scripts": {
+    "start": "nodemon --exec ts-node index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
},

これで、起動するときは、次のコマンドで済みます。

npm start

準備がやや長くなりましたが、次の項目から早速実装を始めていきます。

スキーマを作る

スキーマを作ってみます。
今回は、基本的な型である、オブジェクト型をやってみます。

index.ts
let sampleUsers = [
  {
    id: 1,
    name: 'user_1',
    comment: 'hello',
  },
  {
    id: 2,
    name: 'user_2',
    comment: 'hello world',
  },
  {
    id: 3,
    name: 'user_3',
    comment: 'bye',
  },
];

const schema: GraphQLSchema = buildSchema(`
  type SampleUser {
    id: Int,
    name: String,
    comment: String
  }

  type Query {
    hello: String
    sampleUsers: [SampleUser]
  }

  type Mutation {
    addSampleUser(name: String, comment: String): SampleUser
  }
`);

1つずつ簡単に説明しておきます。

先に宣言している配列sampleUsersの要素がオブジェクトになっているのですが、その型を定義しています。

type SampleUser {
    id: Int,
    name: String,
    comment: String
}

idフィールドはInt型、nameフィールドとcommentフィールドは両方String型になっています。

また、QueryMutationといったオペレーションの型もオブジェクト型です。
Queryはこんな感じです。

type Query {
    hello: String
    sampleUsers: [SampleUser]
}

実行すると、hellosampleUsersの2つのQueryの返り値の型定義が行われています。
helloは文字列を返すのでString型で定義されており、sampleUsersSampleUser型の配列(sampleUsers)を返すので、[sampleUser]というように配列で返すことがわかるように定義されています。

次は、Mutationです。

type Mutation {
    addSampleUser(name: String, comment: String): SampleUser
}

sampleUsersを増やす、addSampleUserというMutationです。引数を使っています。
引数として、String型のname、同じくString型のcommentを渡します。
追加したオブジェクトを返すので、返り値の型はSampleUserになっています。

リゾルバを作る

それでは、処理の中身リゾルバの方を作っていきます。

index.ts
const root = {
  hello: () => {
    return `Hello!`;
  },
  sampleUsers: () => {
    return sampleUsers;
  },
  addSampleUser: (args: { name: string; comment: string }) => {
    const id = sampleUsers.length === 0 ? 1 : sampleUsers.length + 1;
    const addUser = {
      id: id,
      name: args.name,
      comment: args.comment,
    };
    sampleUsers = [...sampleUsers, addUser];
    return addUser;
  }
};

Queryが2つ、Mutationが1つ定義されています。
それぞれ簡単に説明していきます。

【Query】hello

まずは、Queryのhelloです。
helloを実行すると、「Hello!」という文字列が返ってきます。

index.ts
hello: () => {
    return `Hello!`;
},

【Query】sampleUsers

次に、こちらもQueryのsampleUsersです。
sampleUsersを実行すると、配列sampleUsersが全て返ってきます。

index.ts
sampleUsers: () => {
    return sampleUsers;
},

【Mutation】addSampleUser

最後は、MutationのaddSampleUserです。
addSampleUserを実行すると、nameフィールドとcommentフィールドを持つオブジェクトを引数として渡し、配列sampleUsersに追加を行なっています。
追加したオブジェクトを返します。

index.ts
addSampleUser: (args: { name: string; comment: string }) => {
    // idは、配列の要素が0個であれば1、1つ以上要素があれば配列の長さ+1の値とする。
    const id = sampleUsers.length === 0 ? 1 : sampleUsers.length + 1;
    const addUser = {
      id: id,
      name: args.name,
      comment: args.comment,
    };
    sampleUsers = [...sampleUsers, addUser];
    return addUser;
  }

実行してみる

実行に移っていきたいと思います。

実行のために追記

以下のコードを追記します。

index.ts
app.use(
  '/graphql',
  graphqlHTTP({
    schema: schema,
    rootValue: root,
    graphiql: true,
  })
);

/graphqlでエンドポイントを指定していますが、こちらの指定はなくても動きます。
ミドルウェアgraphqlHTTPに渡しているのは、上で定義したスキーマ(schema)・リゾルバ(rootValue)・graphiqlです。スキーマやリゾルバについてはすでに紹介しているのでわかるかと思います。
最後のgraphiqlは、「GraphiQL」(グラフィカル)というブラウザ上でGraphQLのクエリを実行することができるツールがあるのですが、それを使うかどうかを真偽値で指定しています。今回はtrueにしているので、使用します。

実行!

いよいよ、実行していきます。
以下のコマンドで実行します。

npm start

http://localhost:3000/graphqlで、以下の画像のようなGraphiQLの画面が開けると思います。

GraphiQLの画面左側にクエリや引数を記述し、上部のボタンで実行します。実行結果は、画面右側に出てきます。

クエリを1つずつ実行してみます。

【Query】hello

Queryのhelloを実行します。
GraphiQLの画面左側に以下のように記述します。

query hello {
    hello
}

すると、次のようになります。

きちんと、「hello!」が返ってきています。

【Query】sampleUsers

続いて、QueryのsampleUsersを実行します。
GraphiQLの画面左側に以下のように記述します。

query sampleUsers {
  sampleUsers {
    id
    name
    comment
  }
}

helloとは異なり、sampleUsersの場合は取得するデータを指定することができます。上記だと、オブジェクト型SampleUserのオブジェクトが持つ全てのフィールド(idnamecomment)を返しています。
実行すると、次のようになります。

idnamecommentの3つを持つオブジェクトを要素とする配列が返ってきます。

また、以下のように特定のフィールドだけを返すように指定することも可能です。

query sampleUsers {
  sampleUsers {
    name
  }
}

上記では、nameフィールドだけを返すように指定しています。
実行すると、次のようになります。

nameフィールドのみを持つオブジェクトを要素とする配列が返ってきました。
このように、必要な情報だけを取得することも可能なのです。

【Mutation】addSampleUser

MutationのaddSampleUserを実行します。
GraphiQLの画面左側に次のように記述します。

mutation addSampleUser ($name: String, $comment: String) {
  addSampleUser (name: $name, comment: $comment) {
    id
    name
    comment
  }
}

上記だけでは、引数の値が指定できていないので足りません。下にある、QUERY VARIABLESに次のように記述して、引数の値を指定します。

{
  "name": "user_4",
  "comment": "Hi!"
}

引数であるnamecommentを、JSON形式で指定します。
これで実行すると、次のようになります。

追加された、nameuser_4で、commentHi!のオブジェクトが返ってきています。idもきちんと配列の要素の数+1になっているようです。
ここでももちろん、返すデータを減らすことが可能です。

念の為、ユーザがきちんと追加されたのかを確認しておきます。
QueryのsampleUsersを先ほどの説明と同じようにやってみます。すると、次のようになります。

先ほど追加した内容で、要素が1つ増えています。無事、ユーザが追加されたことがわかります。

実践編!!

ここからは「実践編」ということで、バックエンド・フロントエンド両方を実装し、開発をしてみます。もちろん、DBも使います。

使用したライブラリ等

バックエンド

バックエンドは、こんな感じです。

  • TypeScript
  • Express
  • Apollo Server
  • GraphQL
  • Prisma

フロントエンド

フロントエンドは、こんな感じです。

  • TypeScript
  • React
  • Apollo Client
  • GraphQL
  • Emotion

DB

DBは、PostgreSQLを使いました。

  • PostgreSQL

実は、元々Azure Database for PostgreSQLを使っていたのですが、記事作るまでに無料使用できる期間が過ぎちゃいました...。

バックエンドの実装

API実装していきます!

とりあえず準備する

まずは、準備をしていきます。

インストール

必要なパッケージ等のインストールから始めていきます。

  • TypeScript
npm install --save-dev typescript ts-node @types/node
  • Express
npm install express @types/express
  • Apollo ServerとGraphQL
npm install @apollo/server graphql
  • Prisma
npm install --save-dev prisma
  • そのほか
npm install --save-dev body-parser cors nodemon @types/cors
起動スクリプトを追加

また、package.jsonに起動スクリプトを追加しておきます。

package.json
"scripts": {
+    "start": "nodemon --exec ts-node --esm index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
},

DB

今回、DBはPostgreSQLを使いました。
Azure Database for PostgreSQLやAmazon RDS for PostgreSQLなど色々あると思いますが、お好みで選択して頂いて大丈夫です。
私は、renderでDB作成を行いました。

Prisma

Prismaを使っていくので、設定を行っていきます。
Prismaとは、ORM(Object Relational Mapper)で、DB操作をオブジェクトを扱うときと同じように、オブジェクトのメソッドを通して行うことができる技術を使ったライブラリです。

##### 初期化
まずは、初期化をします。

npx prisma init

初期化することで、prismaフォルダとenvファイルが作成されます。prismaフォルダの中には、schema.prismaというファイルがあります。schema.prismaはこんな感じになっているかなと思います。

schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
DB接続情報を設定する

DBの接続情報を設定します。datasourceurlにあるように、envファイルにDBの接続情報を入れることで接続することが可能です。
envファイルにDATABASE_URLを追加します。

.env
DATABASE_URL={DBのURL}?schema=public

DATABASE_URLに、作成したDBのURLを渡します。PostgreSQLだと、postgres://で始まるURLです。
また、末尾に、クエリ文字列でschema=publicを追加します。

データモデルを定義する

データモデルを定義していきます。テーブルの構成や各カラムのデータ型を定義します。

schema.prisma
model task {
  id       Int    @id @default(autoincrement())
  title    String
  deadline String
}

ここでは、Int型のid・String型のtitle・String型のdeadlineの3つのカラムを持つ、taskテーブルを定義しています。

マイグレーションする

schema.prismaで必要な内容は記述できたので、マイグレーションをしてテーブルの作成を行います。
次のコマンドを実行します。

npx prisma migrate dev

これで、schema.prismaをもとにテーブルの作成が行われました。

Prisma Clientの導入

Prisma Clientとは、自動生成される型安全なクエリビルダです。
次のコマンドを実行します。

npx prisma generate

このコマンドを実行することで、Prisma Clientのコードの生成だけでなく、@prisma/clientがまだインストールされていなければインストールまでしてくれます。

また、schema.prismaに変更を加えるたびに、このコマンドを実行した上でマイグレーションを行うと、Prisma Clientのコードも更新できます。

GraphQLを使ってAPI実装

DB関係は準備できたので、ここからは、GraphQLを使い、APIを実装していきます。

index.ts
import express from 'express';
import { ApolloServer, BaseContext } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
import bodyParser from 'body-parser';
import { PrismaClient } from '@prisma/client';

const prisma: PrismaClient = new PrismaClient();

const app = express();
const port: number = 8000;

// ここに今からコードを書いていく。

// サーバ起動
await new Promise<void>((resolve) => app.listen({ port: port }, resolve));
console.log(`🚀 Server ready at http://localhost:${port}/`);
スキーマ

まずは、スキーマです。
スキーマはデータの型や返す値の型を定義するところです。

index.ts
const typeDefs = `
  type Task {
    id: Int!
    title: String!
    deadline: String
  }

  type Query {
    hello(name: String): String
    greeting: String
    getTasks: [Task]
  }

  type Mutation {
    addTask(title: String!, deadline: String): Task
    deleteTask(id: Int!): Task
    updateTask(id: Int!, title: String!, deadline: String): Task
  }
`;

変数typeDefsに、テンプレートリテラルで記述したスキーマを渡します。

オブジェクト型のTask、Query型、Mutation型が定義されています。
オブジェクト型のTaskは、Int型のidフィールド、String型のtitleフィールド、String型のdeadlineを持つことがわかります。idフィールドとtitleフィールドには、!が付いています。これは、non-nullable fieldであり、nullを入れられないfieldであることを表しています。

また、Queryは、String型の値を返すhellogreetingや、オブジェクト型のTaskを要素とする配列を返すgetTasksの5つが定義されています。Mutationは、オブジェクト型のTaskで返すaddTaskdeleteTaskupdateTaskの3つが定義されています。
これらの中には、QueryのhelloやMutationのaddTaskのように、引数を持つものもあることが上記のコードからわかると思います。

リゾルバ

続いて、リゾルバを作っていきます。
リゾルバは、QueryやMutationなどを実行したときに行われる具体的な処理を記述します。

...の前に、リゾルバの中で使う配列や関数などを先に定義しておきます。

index.ts
const greetings: string[] = [
  'Hello!',
  '¡Hola!',
  'こんにちは!',
  '你好!',
  'bonjour!',
];

// ランダムな数字を返す
const getRandomValue = (max: number): number => {
  return Math.floor(Math.random() * max);
};

気を取り直して、リゾルバの定義をやっていきます。

index.ts
const resolvers = {
  Query: {
    hello: (parent: any, args: { name: string }) => {
      return `Hello, ${args.name}!`;
    },
    // ランダムであいさつを返してもらう。
    greeting: () => {
      const max: number = greetings.length;
      return greetings[getRandomValue(max)];
    },
    // タスク全部取得
    getTasks: () => prisma.task.findMany(),
  },
  Mutation: {
    // タスク登録
    addTask: (parent: any, args: { title: string; deadline: string }) => {
      return prisma.task.create({
        data: {
          title: args.title,
          deadline: args.deadline,
        },
      });
    },

    // タスク削除
    deleteTask: (parent: any, args: { id: number }) => {
      return prisma.task.delete({
        where: {
          id: args.id,
        },
      });
    },

    // タスク更新
    updateTask: (parent: any, args: { id: number; title: string; deadline: string }) => {
      return prisma.task.update({
        where: {
          id: args.id,
        },
        data: {
          id: args.id,
          title: args.title,
          deadline: args.deadline,
        },
      });
    },
  }
};

QueryとMutationをそれぞれざっくりと見ていきます。
まずは、Queryから。

index.ts
Query: {
    hello: (parent: any, args: { name: string }) => {
      return `Hello, ${args.name}!`;
    },
    // ランダムであいさつを返してもらう。
    greeting: () => {
      const max: number = greetings.length;
      return greetings[getRandomValue(max)];
    },
    // タスク全部取得
    getTasks: () => prisma.task.findMany(),
},

helloでは、任意の名前を引数として受け取って、それを使って文字列「Hello, {引数で受け取った名前}!」を返します。
greetingでは、先に定義した配列greetingsの要素をランダムで取得して返します。
getTasksでは、タスクを全件取得して返しています。こちらは、DBのtaskテーブルに格納されているレコードをPrismaを使って取得してきています。findManyは、Prisma Clientが提供している関数で、複数件のレコード取得に使います。

次は、Mutationです。
それぞれ見ていきます。

まず、タスク登録で使うaddTask

// タスク登録
addTask: (parent: any, args: { title: string; deadline: string }) => {
    return prisma.task.create({
    data: {
        title: args.title,
        deadline: args.deadline,
    },
    });
},

こちらは、引数として、文字列titleと文字列deadlineを受け取ります。
create関数は、Prisma Clientが提供する関数で、レコードを追加するときに使います。引数のオブジェクトのdataプロパティに、以下のように追加する値を渡します。カラム名をプロパティとし、プロパティの値は追加したい値を入れます。

data: {
    title: args.title, // titleに追加する。
    deadline: args.deadline, // deadlineに追加する。
}

プロパティの値に、引数として受け取った、titledeadlineの値を渡しています。

次は、タスクを削除するときに使うdeleteTask

// タスク削除
deleteTask: (parent: any, args: { id: number }) => {
    return prisma.task.delete({
    where: {
        id: args.id,
    },
    });
},

こちらは、引数に数値idを受け取ります。
delete関数も、Prisma Clientが提供する関数で、レコードを削除するときに使います。引数のオブジェクトのwhereプロパティに、以下のように一意のレコードを識別できる値を渡します。カラム名をプロパティとし、プロパティの値は一意のカラムの値を入れます。

where: {
    id: args.id, // 一意のレコードを識別するカラムはidとする。
}

プロパティの値として、引数として受け取ったidの値を渡しています。つまり、idカラムの値が、引数として渡したidの値と一致するレコードが削除されるということになります。
SQLのWHERE句にとても似ているので、わかりやすいのではないでしょうか??

そして、最後は、updateTaskです。

// タスク更新
updateTask: (parent: any, args: { id: number; title: string; deadline: string }) => {
    return prisma.task.update({
    where: {
        id: args.id,
    },
    data: {
        id: args.id,
        title: args.title,
        deadline: args.deadline,
    }
    });
}

こちらは、引数に数値id・文字列title・文字列deadlineを受け取ります。titledeadlineは更新したい新しい値が入ってきます。
update関数も、Prisma Clientが提供している関数で、既存のレコードの値を更新するときに使います。引数には、whereプロパティとdataプロパティを渡しています。whereプロパティには、delete関数のときと同じように、更新対象のレコードを識別するための一意の値(ここではid)を、dataプロパティには、各カラムに入れる新しい値を渡します。

起動の準備

スキーマ・リゾルバが用意できたので、動かせるようにミドルウェアの設定などを行います。

index.ts
// ApolloServer初期化
const server = new ApolloServer<BaseContext>({
  typeDefs,
  resolvers,
});

// ApolloServer起動
await server.start();

// Expressのミドルウェア設定
app.use(
  '/api',
  cors<cors.CorsRequest>(),
  bodyParser.json(),
  expressMiddleware(server)
);

ApolloServerの初期化や起動を行います。
エンドポイントは、/apiとしています。GraphQLは、シングルエンドポイントなので、上で定義したどのQuery、どのMutationを実行しても、エンドポイントは必ず/apiということになります。
expressMiddlewareの引数に初期化したApolloServerを渡してExpressのミドルウェアの設定を行います。フロントとの接続時にCORSで引っかかるので、corsを渡します。

これで、バックエンド方は、準備できました👏

フロントエンドの実装

バックエンドの実装は終わりましたが、APIを作っただけなので、まだ何がなんだかわからないですね。
続いて、フロントエンドの実装を見ていきます。

とりあえず準備する

必要なライブラリ等のインストールを行います。
create-react-appを使用していますが、その際にtypescriptを選択しており手動導入していないため、Typescriptのインストールコマンドは省きます。

  • GraphQL
npm install graphql
  • Apollo Client
npm install @apollo/client

これ以降のライブラリは、本記事のテーマ的には重要ではないのですが、一応記載しておきます。

  • React Hook Form・React Datepicker・date-fns
npm install --save-dev react-hook-form date-fns react-datepicker @types/react-datepicker
  • React Router
npm install react-router-dom
  • Recoil
npm install recoil
  • Emotion
npm install @emotion/react

Apollo Clientを初期化する

Apollo Clientを使うための設定からやっていきます。

ApolloClientの初期化を行います。

apolloClient.ts
import {
  ApolloClient,
  InMemoryCache,
  NormalizedCacheObject,
} from '@apollo/client';

const apolloClient: ApolloClient<NormalizedCacheObject> = new ApolloClient({
  uri: process.env.REACT_APP_API_URL,
  cache: new InMemoryCache(),
});

export default apolloClient;

引数として渡しているオブジェクトには、uriプロパティとcacheプロパティが含まれています。
このuriプロパティには、GraphQLサーバのURLを指定します。上記では、envファイルに記載しています。
また、cacheプロパティには、InMemoryCacheのインスタンスが渡されています。このInMemoryCacheは、キャッシュで利用します。

App.tsxで、ApolloProviderコンポーネントでclientapolloClientを渡したら、フロントでApollo Clientを使用できるようになります。

App.tsx
import Router from './Router';
import { ApolloProvider } from '@apollo/client';
import apolloClient from './apolloClient';

const App = () => {
  return (
    <div>
      <ApolloProvider client={apolloClient}>
        <h1>GraphQLに挑戦!</h1>
        <Router />
      </ApolloProvider>
    </div>
  );
};

export default App;

タスク管理 Tasksの実装

ここからは、各機能の実装を見ていきます。
まずは、タスク管理をする「Tasks」からです。

TasksのindexコンポーネントTasksIndex.tsxを見てみます。

TasksIndex.tsx
/** @jsxImportSource @emotion/react */

import Layout from '../../components/Layout';
import PageTitle from '../../components/PageTitle';
import InputForm from './components/InputForm';
import TaskList from './components/TaskList';
import { contentsWrapper } from './styles/taskIndex';

const TasksIndex = () => {
  return (
    <Layout>
      <PageTitle pageTitle='Tasks' />
      <div css={contentsWrapper}>
        <InputForm />
        <TaskList />
      </div>
    </Layout>
  );
};

export default TasksIndex;

スタイリング関連のコンポーネントを除いて、InputFormコンポーネントとTaskListコンポーネントの2つがあります。

タスク追加をできるようにする!(InputFormコンポーネント)

InputFormコンポーネントでは、タスクの追加を行います。
追加処理の部分以外は、次のような感じです。

InputForm.tsx
import { Controller, useForm } from 'react-hook-form';
import DatePicker, { registerLocale } from 'react-datepicker';
import ja from 'date-fns/locale/ja';
import { format } from 'date-fns';
import { useMutation } from '@apollo/client';
import { ADD_TASK, GET_TASKS } from '../../../ts/gql';

type InputDataType = {
  title: string;
  deadline: Date;
};

type FormDataParam = {
  title: string;
  deadline: string;
};

const InputForm = (): EmotionJSX.Element => {
  const { register, control, handleSubmit, reset } = useForm<InputDataType>();

  // DatePicker用にロケーションをjaにセット
  registerLocale('ja', ja);

  // 追加ボタン押下で、タスク追加処理を実行
  const onSubmit = (data: InputDataType): void => {
    // タスクの追加処理はここでやります。
  };

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor='taskTitle'>
            タスク:
          </label>
          <input
            type='text'
            id='taskTitle'
            {...register('title')}
            placeholder='入力してください'
          />
        </div>
        <div>
          <label htmlFor='deadline'>
            期限:
          </label>
          <Controller
            control={control}
            name='deadline'
            rules={{
              required: true,
            }}
            render={({ field: { onChange, value = new Date() } }) => (
              <DatePicker
                showIcon
                id='deadline'
                locale='ja'
                selected={value}
                onChange={(date) => {
                  date && onChange(date);
                }}
              />
            )}
          />
        </div>
        <button type='submit'>
          追加
        </button>
      </form>
    </div>
  );
};

export default InputForm;

スタイリング済みですが、こんな感じです。

それでは、Apollo Clientを利用してタスクの追加処理を作成していきます。
src/ts/gql/index.tsを作り、クエリを書いていきます。

src/ts/gql/index.ts
import { DocumentNode, gql } from '@apollo/client';

export const ADD_TASK: DocumentNode = gql`
  mutation AddTask($title: String!, $deadline: String) {
    addTask(title: $title, deadline: $deadline) {
      id
      title
      deadline
    }
  }
`;

GraphiQLで実行したときのような書き方をします。タスク追加は、データの追加になるので、mutationです。
gqlはパースするクエリ文字列を囲むタグ関数です。
取得するカラムは、今回はidtitledeadlineの3つ全てです。もちろん、ここで特定のカラムのみ返すようにすることも可能です。

クエリを書けたので、コンポーネントで実際に使っていきます。
InputFormコンポーネントに戻り、次のコードを追加します。

InputForm.tsx
const InputForm = (): EmotionJSX.Element => {
  const { register, control, handleSubmit, reset } = useForm<InputDataType>();
+ const [addTask, { loading, error }] = useMutation(ADD_TASK);

  // DatePicker用にロケーションをjaにセット
  registerLocale('ja', ja);

<中略>

};
export default InputForm;

useMutationというフックを使います。引数に先ほど記述したクエリを渡しています。addTaskは引数として渡されたクエリを実行する関数です。loadingerrorには、ローディング中かどうかの真偽値やエラーメッセージが入って来ます。

タスク追加処理を実行する、onSubmit関数の中身も書いていきます。

InputForm.tsx
const onSubmit = (data: InputDataType): void => {
+    const param: FormDataParam = {
+      title: data.title,
+      deadline: format(data.deadline, 'yyyy/MM/dd'),
+    };
+
+    // タスク追加実行
+    addTask({ variables: param });
+
+    // フォームをリセット
+    reset();
};

addTask関数に、入力した値をまとめたparamを渡しています。variablesというプロパティの値とし、オブジェクトを引数とします。
これで、タスクを追加する処理を実装できました。

試しにタスクを追加してみましたが、今はまだ一覧表示を実装していないので、追加されているのかを画面上で確認することができません。ですが、以下のコマンドを実行すると、http://localhost:5555が起動して、DBに入っているデータをブラウザ上で簡単に確認できるようになります。

npx prisma studio

Prisma Studioというツールで、DB上のデータを確認したり編集したりできます。
確認してみると、次のようになっていました。

無事、タスクが追加されているようです👏

タスク一覧表示をできるようにする!(TaskListコンポーネント)

追加できるようになったので、一覧で見たいですね。
タスク取得のクエリを用意しておきます。

ts/gql/index.ts
export const GET_TASKS: DocumentNode = gql`
  query GetTasks {
    getTasks {
      id
      title
      deadline
    }
  }
`;

QuerygetTasksを使います。DBにあるタスクを全て取得してくるので、引数は不要です。

それでは、TaskListコンポーネントを作っていきます。

TaskList.tsx
import { useQuery } from '@apollo/client';
import { GET_TASKS } from '../../../ts/gql';

const TaskList = (): EmotionJSX.Element => {
  const { loading, error, data } = useQuery(GET_TASKS);

  return (
    <div>
      {loading && <p>Loading...</p>}
      {!loading &&
        (data.getTasks.length === 0 ? (
          <p>表示するタスクは現在0です。</p>
        ) : (
          <table>
            <thead>
              <tr>
                <th>id</th>
                <th>タイトル</th>
                <th>期限</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {/* ローディング終わるのを待たないと、undefinedが返ってくる */}
              {data.getTasks.map((task: any) => (
                <tr key={task.id}>
                  <td>{task.id}</td>
                  <td>{task.title}</td>
                  <td>{task.deadline}</td>
                  <td>
                    <button>編集</button>
                    <button>削除</button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        ))}
      {error && <p>Error is occured!</p>}
    </div>
  );
};

export default TaskList;

今回はデータ取得のQueryを使うので、useQueryフックです。引数には、GET_TASKSを渡します。
useQueryの場合は、dataに実行結果が入ってくるので、それをmapで展開することで一覧表示することが可能になります。
今回の場合、data.getTasksを展開することで、一覧表示できます。

タスクを追加してみると、次のようになりました。

無事、タスクを一覧で見ることができました👏

タスクを削除できるようにする!(TaskListコンポーネント)

タスク追加とタスク削除を見てきました。これでQueryMutationの両方を確認できました。
次に、タスク削除を実装してみます。

クエリを先に準備しておきます。

ts/gql/index.ts
export const DELETE_TASK: DocumentNode = gql`
  mutation DeleteTask($id: Int!) {
    deleteTask(id: $id) {
      id
      title
      deadline
    }
  }
`;

削除するレコードを指定する必要があるので、引数にidを受け取っています。

TaskListコンポーネントにも追加します。

TaskList.tsx
import { useQuery } from '@apollo/client';
+ import { GET_TASKS, DELETE_TASK } from '../../../ts/gql';

const TaskList = (): EmotionJSX.Element => {
  const { loading, error, data } = useQuery(GET_TASKS);
+ const [deleteTask] = useMutation(DELETE_TASK, {
+   // 削除が実行されたら、再度タスク一覧を取得する
+   refetchQueries: [GET_TASKS, 'GetTasks'],
+ });

+ // 削除ボタン押下で、タスク削除処理を実行
+ const handleDeleteTask = (id: number): void => {
+   const param: DeleteParam = {
+      id: id,
+   };
+
+   // タスク削除実行
+   deleteTask({ variables: param });
+ };

  return (
    <div>
        <中略>
            <tbody>
              {/* ローディング終わるのを待たないと、undefinedが返ってくる */}
              {data.getTasks.map((task: any) => (
                <tr key={task.id}>
                  <td>{task.id}</td>
                  <td>{task.title}</td>
                  <td>{task.deadline}</td>
                  <td>
                    <button>編集</button>
                    <button
+                     onClick={() => handleDeleteTask(task.id)}
                    >
                      削除
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
        <中略>
    </div>
  );
};

export default TaskList;

削除ボタン押下で、handleDeleteTask関数が実行されます。引数には、タスクのidを渡しています。useMutationについてはすでに確認しているのでここまではわかると思います。

少し違うのは、useMutationの引数です。第2引数にオブジェクトが渡されています。これは、refetchQueriesというプロパティを持つオブジェクトで、DELETE_TASKを実行したのちに実行するクエリを指定しています。ここでは、GET_TASKSを指定しており、タスクを削除したのちに再度タスクを全件取得し直しています。これにより、タスクを削除してすぐに削除後のテーブルに格納されているレコードが表示に反映されます。
このrefetchQueriesは、タスク追加時にも有効です。タスクを追加してすぐに一覧に表示させることが可能になるためです。

削除ボタンを押下して、タスクを1件削除してみると、次のようになりました。

無事、1件削除されています👏

ほかにも編集状態や編集時のレコード更新など色々と実装しましたが、ここまででQueryMutationについて大体わかって頂けたかな〜と思うので、説明は端折らせて頂きます🙏

おわりに

今回は、GraphQLを使ってみました👏
気になっていましたが、ずっと使えていなかったのですが、今回お仕事の関係もあり、すぐに勉強しないといけない状況になりました。ギリギリになって切羽詰まらないとやらないってやつですかね??笑
折角なので、勉強したことを記事として簡単にまとめてみました。記事の分量的に端折ってしまった部分も多々あるのですが、ざっくりと「GraphQLって大体こんなもんか〜!」という感じで伝われば良いなと思っております!!

もし、表現や内容に不備があった場合は、コメントお願い致します🙏
長くなりましたが、お読み下さりありがとうございました!

参考資料

GraphQL 公式ドキュメント
Prisma 公式ドキュメント
Apollo 公式ドキュメント
これを読めばGraphQL全体がわかる。GraphQLサーバからDB、フロントエンド構築
GraphQLをNode.jsとexpressでためしてみる

参考文献

加藤尋樹 (2021)「特集2 GraphQL完全ガイド」,『WEB+DB PRESS』vol.125 技術評論社 pp.42-74

GitHubで編集を提案

Discussion