【第5回】GraphQLの実践的テクニック - ページネーション、キャッシュ、認証
これまでの4回にわたり、GraphQLの基本概念からサーバー構築、フロントエンドでの利用方法までを学んできました。最終回となる今回は、実世界のアプリケーション開発で不可欠となる、より高度で実践的なテクニックを3つ紹介します。
- ページネーション: 大量のデータを効率的に扱うための必須機能
- キャッシュ: アプリケーションのパフォーマンスを最大化する鍵
- 認証・認可: セキュアなAPIを実現する方法
1. ページネーション (Pagination)
ブログの投稿や商品リストのように、大量のデータセットを一度に全て返すのは非効率です。そこで、データを分割して少しずつ取得する「ページネーション」が必要になります。GraphQLでは、主に2つの方式が用いられます。
a) オフセットベース・ページネーション
最もシンプルな方法で、offset
(スキップする件数)とlimit
(取得する件数)を指定します。
スキーマ定義:
type Query {
posts(limit: Int, offset: Int): [Post]
}
リゾルバの実装例 (SQLライク):
resolvers: {
Query: {
posts: (parent, { limit = 10, offset = 0 }) => {
// 例: DB.select().from('posts').limit(limit).offset(offset)
return db.posts.slice(offset, offset + limit);
}
}
}
この方法は実装が簡単ですが、リストの途中に追加や削除が発生した場合に、ページの重複や欠落が起こりうるという欠点があります。
b) カーソルベース・ページネーション
より堅牢な方法で、リスト内の特定の位置を示す一意の識別子(カーソル)を使います。after
カーソルを指定することで、そのカーソルの次の要素からfirst
件取得する、という動作になります。
スキーマ定義 (Relay仕様): RelayというFacebook製のフレームワークが提唱した仕様がよく使われます。
type Query {
posts(first: Int, after: String): PostConnection!
}
# 個々の要素とカーソルのペア
type PostEdge {
cursor: String!
node: Post!
}
# ページの情報のコンテナ
type PostConnection {
edges: [PostEdge]
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
このスキーマは少し複雑ですが、次のページの有無(hasNextPage
)など、リッチなUIを構築するための情報をクライアントに提供できます。
クライアントでの利用 (Apollo Client): Apollo Clientには、このカーソルベース・ページネーションを便利に扱うためのfetchMore
関数やrelayStylePagination
ヘルパーが用意されており、無限スクロールなどの実装を強力にサポートします。
2. キャッシュの最適化
Apollo Clientのキャッシュは非常に強力ですが、その挙動を理解し、最適化することでアプリケーションのパフォーマンスをさらに向上させることができます。
キャッシュの仕組み
Apollo Clientは、クエリのレスポンスを受け取ると、各オブジェクトを__typename
(スキーマの型名)とid
(または_id
)フィールドを元に正規化し、フラットなストアに保存します。この正規化により、あるミューテーションでオブジェクトが変更された場合、そのオブジェクトを表示している全てのコンポーネントが自動で更新される、という恩恵が得られます。
キャッシュポリシーのカスタマイズ
useQuery
のfetchPolicy
オプションで、キャッシュをどのように利用するかを細かく制御できます。
cache-first
(デフォルト): まずキャッシュを探し、なければネットワークリクエスト。network-only
: 常にネットワークリクエストを実行し、結果をキャッシュに書き込む。cache-and-network
: まずキャッシュのデータを返し、その後ネットワークリクエストを実行してデータを更新する。UIの即時応答性とデータの最新性を両立したい場合に有効。no-cache
: データをキャッシュしない。
キャッシュの手動更新
前回学んだように、useMutation
のupdate
オプションを使えば、ミューテーションのレスポンスを使って、キャッシュを直接書き換えることができます。これは不要なネットワークリクエストを完全に排除できるため、最もパフォーマンスの高い方法です。
3. 認証・認可 (Authentication & Authorization)
セキュアなAPIには認証(「あなたは誰か?」)と認可(「あなたは何ができるか?」)が不可欠です。
認証 (Authentication)
GraphQLサーバー自体は認証の仕組みを持ちません。認証は、GraphQLレイヤーの手前にあるミドルウェア(Expressなど)で処理するのが一般的です。
認証フローの例:
- クライアントがユーザー名とパスワードでログイン用のミューテーション(例:
login
)を叩く。 - サーバーは認証情報を検証し、成功したらJWT(JSON Web Token)などの認証トークンを生成する。
- サーバーは生成したトークンを、クライアントに対してhttpOnlyクッキーとして設定する。
- 以降、クライアントからのリクエストには自動でクッキーが添付される。
- サーバーはリクエストを受け取るたびにクッキーを検証し、有効であればユーザー情報を特定してコンテキストオブジェクトに格納する。
WARNING
認証トークンを localStorage
に保存する方法も見られますが、XSS攻撃に対して脆弱なため推奨されません。セキュリティを考慮する場合は、サーバーサイドで httpOnly
属性を付けたクッキーとしてトークンを扱う方法がより安全です。
コンテキストへのユーザー情報追加 (Apollo Server):
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// httpOnlyクッキーからトークンを取得
const token = req.cookies.token || '';
// トークンを検証してユーザー情報を取得 (関数は自前で実装)
const user = await getUserFromToken(token);
// コンテキストオブジェクトにユーザー情報を詰めて返す
return { user };
},
});
認可 (Authorization)
コンテキストに格納されたユーザー情報は、リゾルバ内からアクセスできます。リゾルバは、このユーザー情報に基づいて、処理を実行してよいか(認可)を判断します。
const resolvers = {
Mutation: {
// 3番目の引数がコンテキスト
editPost: (parent, { id, title }, { user }) => {
// ユーザーがログインしているかチェック
if (!user) {
throw new Error('Not authenticated');
}
const post = db.posts.find(p => p.id === id);
// 投稿の所有者か、管理権限があるかなどをチェック
if (post.authorId !== user.id) {
throw new Error('Not authorized');
}
// ... 認可OKなら処理を続行 ...
}
}
}
GraphQL Shieldのようなライブラリを使えば、この認可ロジックをリゾルバから分離し、スキーマ単位で宣言的に記述することも可能です。
基礎編のまとめ
第1回から第5回まで、GraphQLの基礎を学んできました。
- 第1回: RESTとの違いと基本概念を学びました。
- 第2回: Node.jsとApollo Serverでサーバーを構築しました。
- 第3回: スキーマとリゾルバの役割を深く理解しました。
- 第4回: ReactとApollo Clientでフロントエンドを実装しました。
- 第5回: ページネーション、キャッシュ、認証といった実践的なテクニックを習得しました。
ここまでで、GraphQLアプリケーションの基本的な構築ができるようになりました。続く実践編(第6〜10回)では、さらに高度なトピックについて学んでいきます:
- 第6回: パフォーマンス最適化とN+1問題の解決
- 第7回: Subscriptionを使ったリアルタイム機能
- 第8回: 効果的なテスト戦略
- 第9回: セキュリティとベストプラクティス
- 第10回: 本番環境での運用・監視・デバッグ
次回は、GraphQLのパフォーマンス最適化について詳しく解説します。DataLoaderを使ったN+1問題の解決方法を学び、スケーラブルなGraphQLアプリケーションを構築しましょう!