この記事は セゾン情報システムズ Advent Calendar 2023 2日目の記事です。
はじめに
GraphQLはAPIに関わる部分で、開発者にとっての利便性を向上させてくれるツールだと考えています。
例えば、
- クライアント側からのリクエスト過多によるパフォーマンスを改善したい
- 強固な型安全性を担保したい
- Swagger的な役割のものが欲しい
といったニーズに応えてくれると思っています。
ただし、クライアント側、サーバー側両方との連携が要求されるため考慮することは多くなります。
サーバー側であれば、データベース連携がその1つです。
この部分をなるべくシンプルに連携するために、Prismaを使用して検証してみました。
本記事では、GraphQLの基礎について理解を深めるとともに、Apolloを用いてGraphQLサーバーを実装し、Prismaとの連携手順を知ることを目的とします。
前提と対象読者
以下の知識や経験があり、
- Node.jsの基礎知識
- Prismaの基礎知識
以下の考えをお持ちの方。
- GraphQLに興味がある
- GraphQLとデータベースの連携例を知りたい
- データベースには不慣れだが手軽にAPIを実装したい
本記事の目的とゴール
- GraphQLの概要を説明できるようになる
- REST APIとの違いを説明できるようになる
- Apolloサーバーの使い方の紹介
- GraphQLとPrismaの連携設定ができるようになる
GraphQLとは
GraphQLについては、公式にて下記の説明がされています。
GraphQLは、APIのためのクエリ言語であり、既存のデータを使ってクエリを実行するためのランタイムです。
GraphQLは、API内のデータについて完全で理解しやすい記述を提供し、クライアントに必要なものだけを要求し、時間の経過とともに API を進化させることを容易にし、強力な開発者ツールを可能にします。
要約すると、APIへの問い合わせを単純化し、使いやすくしているということです。
そして、クライアント側・サーバー側両方で使用可能で多くの言語をサポートしています。
GraphQLの主な特徴は以下です。
- エンドポイントは1つ
- オーバーフェッチング(余計なデータを取得)せずに済む
- 型指定でデータが明確になる
- GUIで操作できる開発者ツール
REST APIとの違い
GraphQLはREST APIと比較されることが多いです。
REST APIはエンドポイントをサーバー側に設定してエンドポイントに問い合わせが来たら指定のデータを返します。エンドポイント1つ1つに対してリクエストを送る必要があります。
対してGraphQLはエンドポイントは1つです。GraphQL内のschema(スキーマ)とresolver(リゾルバ)に指定の設定をすることにより、必要なデータを取得します。
「エンドポイントを1つにする」「スキーマとリゾルバによる柔軟なデータ取得構造」により、リクエスト過多やオーバーフェッチングといったパフォーマンスの改善が可能になります。
REST APIとの比較は「GraphQLとRESTの比較 - HUSURA」にて詳細に述べられているので参照ください。
Apolloの概要
Apolloは、GraphQLを簡単に扱えるフロントエンド・バックエンドライブラリです。
クライアントとサーバー両方をApolloで実装することができ、それぞれを「Apolloクライアント」「Apolloサーバー」と呼びます。
以降では、Apolloサーバーにフォーカスし、GraphQLのAPIクエリを叩く部分を中心に取り上げます。
また、APIクエリを確認できるGUIツールであるPlaygroundも使用可能です。
Playgroundを使用すると、簡単にAPIテストができます。
Apolloサーバーの作成
チュートリアルを参考に、サーバーを実装します。
Apolloサーバーを起動するためには、typeDefs
のスキーマの定義とそれに対して何かしらの実体をいれていくためのリゾルバ関数が必要になります。
それらを定義したファイルが以下のserver.jsです。
import { ApolloServer, gql } from "apollo-server";
// スキーマの定義
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
const books = [
{
title: "The Awakening",
author: "Kate Chopin",
},
{
title: "City of Glass",
author: "Paul Auster",
},
];
// リゾルバ関数
const resolvers = {
Query: {
books: () => books,
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
必要なパッケージをインストールした後に、以下の手順で上記を実装していきます。
- スキーマの定義
- リゾルバの定義
- ローカルーサーバーの起動
- Playgroundについて
スキーマの定義
スキーマとはデータ構造のことです。
スキーマは実体(値)を入れるための、ひな型のようなものです。
下記のコードでは、Book型の配列をbooksというフィールドに定義しています。
// スキーマの定義
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
Query
はApolloサーバーでの予約語で、「情報を取得する」という意味があります。
リゾルバ
リゾルバとは定義した型(スキーマ)に対して、実態のある値(何かしらの情報や値)をいれてあげることです。
スキーマで定義した、Query
とbooks(フィールド名)
を定義する必要があります。
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
// リゾルバ関数
const resolvers = {
Query: {
books: () => books,
},
};
ローカルーサーバーの起動
ApolloServerをインスタンス化し、サーバーを呼び出します。
const server = new ApolloServer({
typeDefs,
resolvers
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
以下を実行し、サーバーが起動したら成功です。
node server.js
🚀 Server ready at http://localhost:4000/`
Playgroundについて
PlaygroundはGUIで操作できる開発者ツールのようなものです。
クエリが正常に作動しているか確認することができます。
サーバーを起動した後に、ブラウザでlocalhost:4000/
を開くと使用できます。
左側(Operation)はクエリ情報、右側(Response)はクエリに対するレスポンス情報が表示されます。
定義したクエリ情報を入力すると、レスポンスが返ってくることが確認できます。
ミューテーション(Mutation)の実装
ミューテーション(Mutation)は登録・更新・削除ができるタイプ属性です。
SQLのCRUD操作と比較すると以下になります。
SQL | GraphQL | |
---|---|---|
取得 | SELECT | Query |
登録 | INSERT | Mutation |
更新 | UPDATE | Mutation |
削除 | DELETE | Mutation |
Mutationを追加して、Playgeroudで動作を確かめます。
スキーマ定義とリゾルバ関数にMutationを追加します。
const typeDefs = gql`
type Mutation {
post(title: String, author: String): Book
}
`;
const resolvers = {
Mutation: {
post: (_parent, args) => {
const book = {
title: args.title,
author: args.author,
};
books.push(book);
return book;
},
},
};
PlaygeroudのDocumentationのRoot Typesにmutation: Mutationが追加されています。
これを選択し、引数など必要な設定を追加します。
最後にOperation下部のVariablesにJSON形式で追加する値を入力します。
実行し成功するとレスポンスが返ってくるとともに、booksに追加されています。
追加されたものはQueryで確認することができます。
Prismaによるデータベースとの連携
GraphQLサーバーを起動しデータの取得、登録ができることを確認しましたが、データの永続化ができていません。
Prismaを使用して、データベースと連携しデータの永続化を試みます。
PrismaはNode.jsを対象としたオープンソースORMです。
Prismaについての詳細は述べませんので、公式ドキュメントもしくはPrismaの導入とメリットを考えるを参照ください。
以下の手順でデータベースと連携します。
- データベースの初期化
- Prismaのスキーマ設定
- マイグレーション
- 永続化処理の追加
- サーバーとPrismaの接続
データベースはMySQLを使用し、Dockerで構築します。
以下のdcoker-compose.ymlを作成後、docker-compose up -d
を実行し事前にMySQLを起動しておきます。
version: '3.9'
services:
db:
image: mysql:8.0
environment:
TZ: Asia/Tokyo
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_HOST: ${MYSQL_HOST}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
ports:
- '3306:3306'
volumes:
- my_volume:/var/lib/mysql
volumes:
my_volume:
データベースの初期化
PrismaCLIとPrismaクライアントをインストールします。
yarn add -D prisma
yarn add @prisma/client
以下で初期化を実行します。
npx prisma init
成功すると、実行環境と同階層にprismaというディレクトリが作成されます。
中にはschema.prismaというファイルが配置されています。
Prismaのスキーマ設定
schema.prismaを編集します。
変更点は以下です。
- datasource db内のproviderをmysqlに変更
- model Bookを追加
modelは、データベースがどういう属性を持っているのかを定義するものです。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Book {
id Int @id @default(autoincrement())
title String
author String
createdAt DateTime @default(now())
}
マイグレーション
作成したmodelとdatasourceをもとにMySQLのSQL文を以下のコマンドで自動生成します。
npx prisma migrate dev --name init
成功すると、prismaディレクトリ内にmigrationsディレクトリが作成されます。
中にはmigration.sqlというファイルが配置されており、この中にSQL文が定義されています。
永続化処理の追加
マイグレーションにより、データベースのひな型は作成できたので実際にデータを入れる仕組みを追加します。
以下のコマンドを実行します。
npx prisma generate
成功すると以下のメッセージが表示されます。
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (v5.6.0) to ./node_modules/@prisma/client in 108ms
Start using Prisma Client in Node.js (See: https://pris.ly/d/client)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
データベースに保存するための処理を定義したscript.jsを作成します。
これにより、ローカルサーバーを閉じてもデータが永続化されます。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const main = async () => {
const newBook = await prisma.book.create({
data: {
title: "クライアントから受けっ取ったtitleの値",
author: "クライアントから受けっ取ったauthorの値",
},
});
const allBooks = await prisma.book.findMany();
};
try {
main();
} catch (error) {
throw error;
} finally {
async () => {
// データベース接続を閉じる
prisma.$disconnect;
};
}
サーバーとPrismaの接続
最後にサーバーとデータベースを接続します。
server.jsにPrismaClientをインスタンス化したものを追加し、QueryやMutation内でPrismaと連携できるようにします。
具体的にはcontextを使用します。cotextによりresolvers関数内のQueryやMutation内でprismaという変数が使えるようになります。
これによりデータの取得や生成がQueryやMutation内で可能になります。
import { ApolloServer } from "apollo-server";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); // 追加
// リゾルバ関数
const resolvers = {
Query: {
books: async (parent, args, context) => {
return context.prisma.book.findMany();
},
},
Mutation: {
post: (_parent, args, context) => {
const newBook = context.prisma.book.create({
data: {
title: args.title,
author: args.author,
},
});
return newBook;
},
},
};
const server = new ApolloServer({
typeDefs: fs.readFileSync(
path.join(fileURLToPath(import.meta.resolve("./schema.graphql"))), // server.jsに定義さていた、スキーマ定義を定義したファイル
"utf-8"
),
resolvers,
context: { // 追加
prisma,
},
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
以上で全ての設定が完了になります。
おわりに
GraphQLはパフォーマンスの向上や、今回は検証しませんでしたがTypeScriptと組み合わせると強力な型定義の恩恵が受けられるのではと感じました。
特にGraphQL code generatorと組み合わせを試してみたいです。
また、Prismaとの連携もシンプルでデータベースに不慣れでも実装できたのは魅力的でした。
今回はサーバー側の簡易的な部分しか触れませんでしたが、今後もクライアント側の実装や他の機能を試して理解を深めたいと考えています。
また、随時本記事に反映し更新予定です。
参考資料
【GraphQL入門】RESTに代わるモダンAPIのGraphQLでニュースアプリAPIを構築しながら基礎を学ぶ入門講座