📈

Apollo Server + Nexus + PrismaでGraphQL開発: Relayに従う2

2021/12/06に公開

この記事は、いかずちさんだー Advent Calendar 5日目の記事です。

趣旨

前回の記事では、NexusでRelayのGUID/Nodeに従うためにどうしたらいいかの説明をしました。
今回の記事では、Relayのページングについて解説したいと思います。
お酒飲みすぎてつらいのできょうは手短に…。

Relayのページング

Relayのページングシステムでは、以下の引数を持ちます。

  • first
  • after
  • last
  • before

このうち、firstafterは、「afterで指定したIDのノードの次のノードから、firstで指定した数のノードを返す」という意味です。
また、lastbeforeは、「beforeで指定したIDのノードの前のノードから、lastで指定した数のノードを(手前方向に)返す」という意味です。
実用的にlastbeforeが何に使われているのかはよく知らないのですが(手前方向にページングしたくなること、ある?)、慣例的に実装されているようなので、実装します。

prisma-relay-cursor-connection

世の中にはPrismaでRelayのページングを実装した、prisma-relay-cursor-connectionというライブラリがあるので、それを利用しましょう。
npm i @devoxa/prisma-relay-cursor-connectionとして、インストールを行います。
また、relayなページングを実装するために、src/schema.tsを以下のように編集します。

src/schema.ts
plugins: {
  connectionPlugin({
    extenedConnection: {
      totalCount: { type: 'Int', requireResolver: false },
    }
  })
}

ここでは、ついでに、totalCountというカラムを追加しています。
これは、ページングを考慮せずにすべてのノードの数を返すためのカラムです。

Queryにprisma-relay-cursor-connectionを適用する

2つ前の記事で紹介した記事のPostのQueryに、prisma-relay-cursor-connectionを適用します。

src/query/post.ts
export const searchPosts = extendType({
  type: 'Query',
  definition(t) {
    t.connectionField('posts', {
      type: 'Post',
      additionalArgs: {
        search: stringArg({ nullable: true }),
      },
      resolve(_root, args, ctx){
        const baseArgs = {
          where: {
            OR: [
              { title_contains: args.search },
              { content_contains: args.search },
            ],
          },
        };
        return findManyCursorConnection<Campaign, {databaseId: bigint}>(
          (_args) => ctx.prisma.post.findMany({..._args, ...baseArgs}),
          () => ctx.prisma.post.count(baseArgs),
          { ...args },
          {
            getCursor: (record) => ({ databaseId: record.databaseId }),
            encodeCursor: (cursor) => Buffer.from('Post:' + cursor.databaseId).toString('base64'),
            decodeCursor: (cursor) => ({databaseId: Buffer.from(cursor, 'base64').toString().split(':')[1]}),
          }
        )
      }
    })
  }
})

だいぶ複雑ですが、以下のような感じです。

  • additionalArgs: first, last等以外の引数の指定です。
  • baseArgs: additionalArgsでwhereorderByを実現します。
  • getCursor: ページングのためのIDを指定します。databaseIdでいいと思います。
  • encodeCursor: ページングのためのカーソルを指定します。我々にはGUIDという武器があるので、それでいいでしょう。
  • decodeCursor: ページングのためのカーソルのデコード方法を指定します。GUIDからdatabaseIdを取得すればよいでしょう。

注意点

以上のようにすることでページングはうまく実装できるのですが、実際に発行されるSQLは確認した方がいいでしょう。
特にorderByをindexではないカラムに指定した場合、速度面に不安があります。
2つ前の記事で指定した、以下の指定によって標準出力にSQLが出力されます。

src/context.ts
prisma.$on('query', (e) => {
  console.log(e)
})

何がうれしいのか

正直サーバ側ではよくわからないのですが、これに従うことでApollo ClientやRelayがクライアントサイドでうまいことしてくれみたいです。
クエリはもちろん、オブジェクトのResolverでも大量のデータが返ってくるリスクがある場合はページングを作成しましょう。

おわりに

きょうは大変短くて申し訳なかったですが、ページングについて解説しました。
次回はバリデーションなどのその他の要素について説明します。

GitHubで編集を提案

Discussion