GraphQLのページネーション機能を実装する方法: Apollo client + express-graphql
Offset-basedのページネーションに対応したGraphQL APIの実装方法と、Apollo clientによるPagenation処理の実装方法を紹介します。
ページネーションとは
ページネーションは、APIでデータベースから大量のデータを取得する際に、一度に取得する範囲を設定して段階的にデータを取得することで、サーバーへの負荷を軽減できる手法のことです。
GraphQLでのページネーション
GraphQLでの代表的なページネーションは以下の2つがあります。
1. Offset-basedページネーション
2. Cursor-basedページネーション
Offset-basedのページネーションは、「何件目から何件目まで」というように指定してデータを取得しようとします。直感的にわかりやすく実装しやすいメリットがあります。
Cursor-basedは、取得済みのデータにIDを付けておき、そのIDをもとにして、まだ取得できていないデータを取得しようとします。
データの件数が定まっていない場合に有用ですが、実装が複雑になるデメリットがあります。
ここでは、GraphQLでOffset-basedページネーションを実装する方法を紹介します。
実装は、バックエンドについては、Express.js(Typescript)で、フロント側は、任意のJavsScriptフレームとします。
バックエンド側でGraphQL対応のAPIを用意する
webアプリケーションの作成
nodejsのwebアプリケーションフレームワークとして、expressを使い、graphqlのリクエストを処理するためのミドルウェアとしてexpress-graphqlを使います。
必要なライブラリのインストールには、
npm install graphql express express-graphql --save
とします。以下の通り、APIサーバーとして、Expres.jsでwebアプリを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import { join } from "path"; import express from 'express'; import { graphqlHTTP } from "express-graphql"; import { makeExecutableSchema } from "graphql-tools"; import { loadSchemaSync } from "@graphql-tools/load"; import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; import { resolvers } from "./resolvers"; const typeDefs = loadSchemaSync(join(__dirname, "./typedefs/**/*.gql"), { loaders: [new GraphQLFileLoader()], }); const schema = makeExecutableSchema({ typeDefs, resolvers, }); const app = express(); app.use( '/', graphqlHTTP((request) => { return { schema: schema, graphiql: true, }; }), ); export default app; |
graphQLのschema作成
以下のようなschemaを作成します。
listUsersに対応したクエリでusersのデータを取得します。
offset-basedのページネイションに対応させるために、引数にoffsetとlimitをつけています。
これで一度に取得するデータを制限します。
1 2 3 4 5 6 7 8 9 10 | type User { id: Int name: String } type Query { listUsers (offset: Int, limit: Int): [User] } |
resolverの作成
上記のschemaに対応したresolverを作成します。
モックデータを返すだけの単純なAPIです。
limitとoffsetがデータを切り出すために使われているのが一目瞭然なのが、offset-basedのメリットです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import mockData from "../mock-data"; const usersDefault = mockData.users export default { Query: { listUsers: async (_parent, args, _context) => { if (args.offset && args.limit) { const end = args.limit + args.offset const start = args.offset return [...usersDefault].slice(start, end) } else { return [...usersDefault] } }, } } |
モックデータには以下のようなものを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | export default { users: [ { id: 0, name: "ken" }, { id: 1, name: "jon" }, { id: 2, name: "bob" }, { id: 3, name: "ame" }, { id: 4, name: "henry" }, { id: 5, name: "ken2" }, { id: 6, name: "jon2" }, { id: 7, name: "bob2" }, { id: 8, name: "ame2" }, { id: 9, name: "henry2" }, ] } |
これだけで、バックエンド側の仕組みは完成です。
以下のようなクエリで、範囲を制限してデータを取得できます。
1 2 3 4 5 6 7 | query { listUsers (offset: 2, limit: 3) { id, name } } |
結果は次のようになり、たしかに、範囲が制限されているのを確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "data": { "listUsers": [ { "id": 2, "name": "bob" }, { "id": 3, "name": "ame" }, { "id": 4, "name": "henry" } ] } } |
Apollo clientによるPagenation処理の実装
ここからフロント側の実装を紹介します。
Apolloの公式ドキュメントや各種の既存の記事では、主にReactでの実装が紹介されています。
この記事では、
apollo/client/core
ライブラリを使うことで、フロントのフレームワークを問わないページネイション処理の方法を示します。ApolloClientの設定に関するコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import fetch from 'cross-fetch'; import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client/core"; import { ApolloLink } from "@apollo/client/core"; import { offsetLimitPagination } from "@apollo/client/utilities"; export const apolloClient = new ApolloClient({ link: new HttpLink({ uri: "http://localhost:4000/", fetch }), cache: new InMemoryCache({ addTypename: false, typePolicies: { Query: { fields: { listUsers: offsetLimitPagination() } } } }) }); const observableQuery = await getObservableQuery(apolloClient, "listUsers", gql` query listUsers ($offset: Int, $limit: Int) { listUsers (offset: $offset, limit: $limit) { id, name } } `); |
この
observableQuery
をフロント側で繰り返し使用して、制限した範囲のデータをキャッシュすることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | await observableQuery.fetchMore({ variables: { offset: 1, limit: 3 } }) const result1 = await observableQuery.getCurrentResult() // ここまでの合計のデータが得られるので、3件のみを取得できる console.log(result1.data["listUsers"]) |
この例では、1~3件目のデータのみを取得しています。フロント側では、この3件のデータしか持っていません。
1 2 3 4 5 6 7 8 9 10 11 12 13 | await observableQuery.fetchMore({ variables: { offset: 4, limit: 3 } }) const result2 = await observableQuery.getCurrentResult() // ここまでの合計のデータが得られるので、6件のみを取得できる console.log(result2.data["listUsers"]) |
さらに、3件のデータをリクエストしました。すると、
observableQuery.getCurrentResult()
によって、合計の6件のデータを得たことになります。これらをフロント側で保存しておけるので必要に応じて画面に表示できます。
以上のように、段階的に繰り返してデータを取得し、取得済みのデータをフロントで保持することが可能になります。
まとめ
Offset-basedのページネーションの実装方法を紹介しました。
サーバー側での実装と、フロント側での実装を両方紹介しましたので、すぐにでも試してみることができると思います。
特定のフレームワークに依拠しないpagination実装は、公式ドキュメントでわかりやすく書かれていないので、この記事が苦労しているエンジニアの助けになれば幸いです。
Author Profile
スターフィールド編集部
SHARE