GraphQL レッスン3
リゾルバ実装
Apollo Serverでリゾルバを実装し、データソースと接続しよう
Apollo Server のセットアップ
Apollo Serverは、GraphQLサーバーを構築するための 最も人気のあるライブラリです。スキーマとリゾルバを定義するだけで、 GraphQL APIサーバーを起動できます。
// パッケージのインストール
// npm install @apollo/server graphql
// server.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
// スキーマ定義
const typeDefs = `
type Book {
id: ID!
title: String!
author: Author!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}
type Query {
books: [Book!]!
book(id: ID!): Book
authors: [Author!]!
}
type Mutation {
addBook(title: String!, authorId: ID!): Book!
}
`;
// サンプルデータ
const books = [
{ id: "1", title: "GraphQL入門", authorId: "1" },
{ id: "2", title: "Apollo実践ガイド", authorId: "1" },
{ id: "3", title: "React設計パターン", authorId: "2" },
];
const authors = [
{ id: "1", name: "田中太郎" },
{ id: "2", name: "佐藤花子" },
];
// リゾルバ定義
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find((b) => b.id === id),
authors: () => authors,
},
Mutation: {
addBook: (_, { title, authorId }) => {
const newBook = {
id: String(books.length + 1),
title,
authorId,
};
books.push(newBook);
return newBook;
},
},
// 型リゾルバ
Book: {
author: (book) => authors.find((a) => a.id === book.authorId),
},
Author: {
books: (author) => books.filter((b) => b.authorId === author.id),
},
};
// サーバー起動
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Server ready at ${url}`);リゾルバ関数の仕組み
リゾルバ関数は4つの引数を受け取ります。これらを使ってデータの取得・加工を行います。
// リゾルバ関数のシグネチャ
// resolver(parent, args, context, info)
const resolvers = {
Query: {
// parent: ルートクエリでは通常 undefined
// args: クエリの引数
// context: リクエスト間で共有するデータ(認証情報、DBなど)
// info: クエリのAST情報(高度な使い方)
user: (parent, args, context, info) => {
console.log(args.id); // クエリ引数
console.log(context.user); // 認証済みユーザー
return context.db.users.findById(args.id);
},
// 引数のフィルタリング
posts: (_, { status, limit = 10 }) => {
let result = posts;
if (status) {
result = result.filter((p) => p.status === status);
}
return result.slice(0, limit);
},
},
// 型リゾルバ:フィールドの解決
User: {
// parent はこの場合 User オブジェクト
fullName: (parent) => {
return `${parent.lastName} ${parent.firstName}`;
},
// 非同期リゾルバ
posts: async (parent, _, context) => {
return await context.db.posts.findByAuthorId(parent.id);
},
// 計算フィールド
postCount: async (parent, _, context) => {
const posts = await context.db.posts.findByAuthorId(parent.id);
return posts.length;
},
},
};parent(第1引数)は、 親フィールドのリゾルバが返した値です。型リゾルバでのデータ解決に重要な役割を果たします。
Context の活用
Contextは、すべてのリゾルバ間で共有されるオブジェクトです。 認証情報、データベース接続、外部APIクライアントなどを格納します。
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { PrismaClient } from "@prisma/client";
import { verifyToken } from "./auth";
const prisma = new PrismaClient();
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
// context 関数はリクエストごとに実行される
context: async ({ req }) => {
// 認証トークンの検証
const token = req.headers.authorization || "";
let user = null;
try {
user = await verifyToken(token.replace("Bearer ", ""));
} catch (e) {
// 未認証の場合は user が null
}
return {
db: prisma, // データベース接続
user, // 認証済みユーザー(またはnull)
};
},
listen: { port: 4000 },
});
// リゾルバで context を使う
const resolvers = {
Query: {
// 認証が必要なクエリ
me: (_, __, context) => {
if (!context.user) {
throw new Error("認証が必要です");
}
return context.db.user.findUnique({
where: { id: context.user.id },
});
},
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.user) {
throw new Error("認証が必要です");
}
return context.db.post.create({
data: {
...input,
authorId: context.user.id,
},
});
},
},
};データソースの接続
リゾルバからデータベースやREST APIなど、さまざまなデータソースに接続できます。 データソースをクラスとして整理するパターンが推奨されます。
// datasources/user-api.ts
import { PrismaClient } from "@prisma/client";
export class UserAPI {
private db: PrismaClient;
constructor(db: PrismaClient) {
this.db = db;
}
async getAll() {
return this.db.user.findMany();
}
async getById(id: string) {
return this.db.user.findUnique({ where: { id } });
}
async create(data: { name: string; email: string }) {
return this.db.user.create({ data });
}
async update(id: string, data: { name?: string; email?: string }) {
return this.db.user.update({ where: { id }, data });
}
async delete(id: string) {
await this.db.user.delete({ where: { id } });
return true;
}
}
// context にデータソースを追加
context: async ({ req }) => {
return {
dataSources: {
userAPI: new UserAPI(prisma),
postAPI: new PostAPI(prisma),
},
};
},
// リゾルバでデータソースを使う
const resolvers = {
Query: {
users: (_, __, { dataSources }) => {
return dataSources.userAPI.getAll();
},
user: (_, { id }, { dataSources }) => {
return dataSources.userAPI.getById(id);
},
},
};N+1問題とDataLoader
GraphQLではネストしたクエリが簡単に書ける反面、N+1問題が発生しやすくなります。DataLoaderを使ってバッチ処理で解決しましょう。
// N+1問題の例
// クエリ:
// query { posts { title author { name } } }
//
// 実行されるSQL:
// 1. SELECT * FROM posts (1回)
// 2. SELECT * FROM users WHERE id = 1 (N回)
// 3. SELECT * FROM users WHERE id = 2
// 4. SELECT * FROM users WHERE id = 3
// → 投稿数Nに応じてクエリが増える!
// npm install dataloader
import DataLoader from "dataloader";
// DataLoader でバッチ処理
const createUserLoader = (db: PrismaClient) => {
return new DataLoader<string, User>(async (userIds) => {
// 1回のクエリで複数ユーザーを取得
const users = await db.user.findMany({
where: { id: { in: [...userIds] } },
});
// IDの順序に合わせてマッピング
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id) || null);
});
};
// context でリクエストごとにLoaderを作成
context: async ({ req }) => ({
loaders: {
userLoader: createUserLoader(prisma),
},
}),
// リゾルバで DataLoader を使う
const resolvers = {
Post: {
author: (post, _, { loaders }) => {
// 同じティック内のリクエストが自動的にバッチ化される
return loaders.userLoader.load(post.authorId);
},
},
};
// 実行されるSQL:
// 1. SELECT * FROM posts
// 2. SELECT * FROM users WHERE id IN (1, 2, 3)
// → たった2回のクエリで済む!DataLoaderはリクエストごとに新しいインスタンスを作成してください。 キャッシュが残ると古いデータを返す原因になります。
まとめ
- Apollo Serverで
typeDefsとresolversを定義してGraphQLサーバーを構築 - リゾルバ関数は
parent、args、context、infoの4つの引数を受け取る - Contextに認証情報やDB接続を格納し、全リゾルバで共有する
- データソースをクラスとして整理し、リゾルバをシンプルに保つ
- N+1問題はDataLoaderでバッチ処理して解決する