ついにGraphQLに入門した!
今さらですが!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では、リクエストのことを「オペレーション」と呼びます。
Query、Mutation、Subscriptionの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
を用意しておきましょう。
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}`);
});
起動スクリプトを用意する
起動するとき、nodemon
やts-node
を打つのも面倒なので、package.jsonに"start"
という起動スクリプトを追加してしまいます。
"scripts": {
+ "start": "nodemon --exec ts-node index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
これで、起動するときは、次のコマンドで済みます。
npm start
準備がやや長くなりましたが、次の項目から早速実装を始めていきます。
スキーマを作る
スキーマを作ってみます。
今回は、基本的な型である、オブジェクト型をやってみます。
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型になっています。
また、Query
やMutation
といったオペレーションの型もオブジェクト型です。
Query
はこんな感じです。
type Query {
hello: String
sampleUsers: [SampleUser]
}
実行すると、hello
とsampleUsers
の2つのQueryの返り値の型定義が行われています。
hello
は文字列を返すのでString型で定義されており、sampleUsers
はSampleUser
型の配列(sampleUsers
)を返すので、[sampleUser]
というように配列で返すことがわかるように定義されています。
次は、Mutation
です。
type Mutation {
addSampleUser(name: String, comment: String): SampleUser
}
sampleUsers
を増やす、addSampleUser
というMutationです。引数を使っています。
引数として、String型のname
、同じくString型のcomment
を渡します。
追加したオブジェクトを返すので、返り値の型はSampleUser
になっています。
リゾルバを作る
それでは、処理の中身リゾルバの方を作っていきます。
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!」という文字列が返ってきます。
hello: () => {
return `Hello!`;
},
【Query】sampleUsers
次に、こちらもQueryのsampleUsers
です。
sampleUsers
を実行すると、配列sampleUsers
が全て返ってきます。
sampleUsers: () => {
return sampleUsers;
},
【Mutation】addSampleUser
最後は、MutationのaddSampleUser
です。
addSampleUser
を実行すると、name
フィールドとcomment
フィールドを持つオブジェクトを引数として渡し、配列sampleUsers
に追加を行なっています。
追加したオブジェクトを返します。
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;
}
実行してみる
実行に移っていきたいと思います。
実行のために追記
以下のコードを追記します。
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
のオブジェクトが持つ全てのフィールド(id
・name
・comment
)を返しています。
実行すると、次のようになります。
id
・name
・comment
の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!"
}
引数であるname
とcomment
を、JSON形式で指定します。
これで実行すると、次のようになります。
追加された、name
がuser_4
で、comment
がHi!
のオブジェクトが返ってきています。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
に起動スクリプトを追加しておきます。
"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
はこんな感じになっているかなと思います。
// 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の接続情報を設定します。datasource
のurl
にあるように、env
ファイルにDBの接続情報を入れることで接続することが可能です。
env
ファイルにDATABASE_URL
を追加します。
DATABASE_URL={DBのURL}?schema=public
DATABASE_URL
に、作成したDBのURLを渡します。PostgreSQLだと、postgres://
で始まるURLです。
また、末尾に、クエリ文字列でschema=public
を追加します。
データモデルを定義する
データモデルを定義していきます。テーブルの構成や各カラムのデータ型を定義します。
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を実装していきます。
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}/`);
スキーマ
まずは、スキーマです。
スキーマはデータの型や返す値の型を定義するところです。
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型の値を返すhello
・greeting
や、オブジェクト型のTask
を要素とする配列を返すgetTasks
の5つが定義されています。Mutationは、オブジェクト型のTask
で返すaddTask
・deleteTask
・updateTask
の3つが定義されています。
これらの中には、Queryのhello
やMutationのaddTask
のように、引数を持つものもあることが上記のコードからわかると思います。
リゾルバ
続いて、リゾルバを作っていきます。
リゾルバは、QueryやMutationなどを実行したときに行われる具体的な処理を記述します。
...の前に、リゾルバの中で使う配列や関数などを先に定義しておきます。
const greetings: string[] = [
'Hello!',
'¡Hola!',
'こんにちは!',
'你好!',
'bonjour!',
];
// ランダムな数字を返す
const getRandomValue = (max: number): number => {
return Math.floor(Math.random() * max);
};
気を取り直して、リゾルバの定義をやっていきます。
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から。
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に追加する。
}
プロパティの値に、引数として受け取った、title
とdeadline
の値を渡しています。
次は、タスクを削除するときに使う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
を受け取ります。title
やdeadline
は更新したい新しい値が入ってきます。
update
関数も、Prisma Clientが提供している関数で、既存のレコードの値を更新するときに使います。引数には、where
プロパティとdata
プロパティを渡しています。where
プロパティには、delete
関数のときと同じように、更新対象のレコードを識別するための一意の値(ここではid
)を、data
プロパティには、各カラムに入れる新しい値を渡します。
起動の準備
スキーマ・リゾルバが用意できたので、動かせるようにミドルウェアの設定などを行います。
// 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の初期化を行います。
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
コンポーネントでclient
にapolloClient
を渡したら、フロントでApollo Clientを使用できるようになります。
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
を見てみます。
/** @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
コンポーネントでは、タスクの追加を行います。
追加処理の部分以外は、次のような感じです。
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
を作り、クエリを書いていきます。
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
はパースするクエリ文字列を囲むタグ関数です。
取得するカラムは、今回はid
・title
・deadline
の3つ全てです。もちろん、ここで特定のカラムのみ返すようにすることも可能です。
クエリを書けたので、コンポーネントで実際に使っていきます。
InputForm
コンポーネントに戻り、次のコードを追加します。
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
は引数として渡されたクエリを実行する関数です。loading
とerror
には、ローディング中かどうかの真偽値やエラーメッセージが入って来ます。
タスク追加処理を実行する、onSubmit
関数の中身も書いていきます。
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コンポーネント)
追加できるようになったので、一覧で見たいですね。
タスク取得のクエリを用意しておきます。
export const GET_TASKS: DocumentNode = gql`
query GetTasks {
getTasks {
id
title
deadline
}
}
`;
QuerygetTasks
を使います。DBにあるタスクを全て取得してくるので、引数は不要です。
それでは、TaskList
コンポーネントを作っていきます。
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コンポーネント)
タスク追加とタスク削除を見てきました。これでQuery
とMutation
の両方を確認できました。
次に、タスク削除を実装してみます。
クエリを先に準備しておきます。
export const DELETE_TASK: DocumentNode = gql`
mutation DeleteTask($id: Int!) {
deleteTask(id: $id) {
id
title
deadline
}
}
`;
削除するレコードを指定する必要があるので、引数にid
を受け取っています。
TaskList
コンポーネントにも追加します。
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件削除されています👏
ほかにも編集状態や編集時のレコード更新など色々と実装しましたが、ここまででQuery
やMutation
について大体わかって頂けたかな〜と思うので、説明は端折らせて頂きます🙏
おわりに
今回は、GraphQLを使ってみました👏
気になっていましたが、ずっと使えていなかったのですが、今回お仕事の関係もあり、すぐに勉強しないといけない状況になりました。ギリギリになって切羽詰まらないとやらないってやつですかね??笑
折角なので、勉強したことを記事として簡単にまとめてみました。記事の分量的に端折ってしまった部分も多々あるのですが、ざっくりと「GraphQLって大体こんなもんか〜!」という感じで伝われば良いなと思っております!!
もし、表現や内容に不備があった場合は、コメントお願い致します🙏
長くなりましたが、お読み下さりありがとうございました!
参考資料
GraphQL 公式ドキュメント
Prisma 公式ドキュメント
Apollo 公式ドキュメント
これを読めばGraphQL全体がわかる。GraphQLサーバからDB、フロントエンド構築
GraphQLをNode.jsとexpressでためしてみる
参考文献
加藤尋樹 (2021)「特集2 GraphQL完全ガイド」,『WEB+DB PRESS』vol.125 技術評論社 pp.42-74
Discussion