Prisma与Relay分页数据格式转换实战:prisma-relay-cursor-connection库详解
2026/5/15 12:12:13 网站建设 项目流程

1. 项目概述:连接Prisma与Relay的桥梁

如果你正在用Prisma构建GraphQL API,并且你的前端用的是Relay,那你大概率遇到过这个头疼的问题:Prisma返回的游标分页数据格式,和Relay期望的连接(Connection)与边(Edge)格式对不上。这就像两个说不同方言的人在交流,虽然都懂“分页”这个概念,但具体的“语法”和“数据结构”完全不兼容。devoxa/prisma-relay-cursor-connection这个库,就是为解决这个“方言不通”的问题而生的。

简单来说,它是一个小巧但功能强大的工具函数库。它的核心工作就是接收Prisma的查询结果,然后按照Relay Cursor Connections Specification(Relay游标连接规范)的要求,进行“翻译”和“包装”,生成包含edgespageInfototalCount等标准字段的连接对象。这样一来,你的GraphQL服务层就可以直接返回这个对象,Relay客户端就能无缝解析和使用。我自己在好几个生产项目中都用过它,从最初的简单列表到后来复杂的嵌套关联分页,它都处理得相当稳健,极大地减少了我们前后端在分页数据对接上的摩擦成本。

这个库适合所有使用Prisma作为ORM、并采用Relay风格GraphQL API的开发者。无论你是刚接触GraphQL分页的新手,还是正在为现有API的分页兼容性头疼的老手,它都能帮你省下大量手动转换数据结构的重复劳动,让你更专注于业务逻辑本身。

2. 核心原理与设计思路拆解

要理解这个库的价值,我们得先掰开揉碎看看Prisma和Relay在分页上到底是怎么“打架”的。

2.1 Prisma的游标分页模式

Prisma的分页非常直观,主要提供两种方式:skip/take(偏移分页)和基于游标的分页。对于需要稳定排序和高效深度分页的场景,游标分页是首选。一个典型的Prisma游标查询长这样:

const results = await prisma.post.findMany({ where: { published: true }, orderBy: { id: 'asc' }, // 必须有一个唯一的排序字段 cursor: { id: lastPostId }, // 上一页最后一个节点的游标 take: 10, // 获取的数量 skip: 1, // 跳过游标本身,从下一个开始 });

Prisma返回的就是一个简单的对象数组Post[]。分页状态(是否有下一页、上一页)需要你根据返回数组的长度(是否小于take值)和游标值来手动推断。它不关心totalCount,也不提供标准的pageInfo结构。

2.2 Relay连接规范的要求

Relay规范定义了一套非常严谨的分页数据结构,旨在解决偏移分页的固有缺陷(如数据项增删导致页面内容错乱)。一个标准的Relay连接类型在GraphQL Schema中定义如下:

type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }

关键差异在于:

  1. 数据包装:原始数据(Post)必须被嵌套在edges数组下的node字段中。
  2. 游标编码:每个edge必须携带一个基于Base64编码的cursor字符串,代表该节点的位置。
  3. 分页信息pageInfo对象必须明确告知客户端是否有前一页和后一页,并提供首尾游标。
  4. 总数统计:可选的totalCount字段对于显示总条目数或实现“跳转到某页”功能很有用。

2.3 库的设计哲学:做纯粹的转换器

prisma-relay-cursor-connection的设计非常克制和清晰。它不试图取代Prisma的查询能力,也不侵入你的GraphQL类型定义。它只做一件事:在Prisma查询执行之后,GraphQL数据返回之前,进行数据格式的转换

它的输入是:

  • 一个Prisma模型查询器(例如prisma.post)。
  • 一套符合Relay规范的查询参数(first,after,last,before)。
  • 可选的额外配置(如自定义游标解析、总数统计逻辑)。

它的输出是:

  • 一个完全符合Relay连接规范的对象,包含edges,pageInfo,totalCount

这种“中间件”或“适配器”的定位,使得它能够轻松集成到任何现有的Prisma + GraphQL架构中,无论是使用Apollo Server、GraphQL Yoga还是其他任何GraphQL服务器实现。

注意:这个库处理GraphQL层的类型定义或解析器(Resolver)签名。你仍然需要自己在Schema中定义XxxConnectionXxxEdge类型。这个库只是在你Resolver的内部逻辑中,帮你生成符合那些类型定义的数据。

3. 核心功能与配置详解

了解了“为什么”需要它之后,我们来看看它具体“能做什么”。这个库主要导出一个函数:findManyCursorConnection。我们将深入它的参数、返回值以及各种配置选项。

3.1 基础调用与参数解析

最基本的用法看起来是这样的:

import { findManyCursorConnection } from '@devoxa/prisma-relay-cursor-connection'; const result = await findManyCursorConnection( (args) => prisma.post.findMany(args), // 第一个参数:Prisma findMany 函数 () => prisma.post.count(), // 第二个参数:获取总数的函数 { first: 10, after: 'YXJyYXljb25uZWN0aW9uOjEw' }, // 第三个参数:Relay风格分页参数 // 第四个参数(可选):配置选项 );

我们来逐一拆解这四个参数:

  1. findManyFn: (args: Prisma.FindManyArgs) => Promise<T[]>这是一个函数,它接收Prisma风格的FindManyArgs(包含where,orderBy,cursor,take,skip等),并返回一个模型数组的Promise。库内部会根据Relay参数计算出合适的Prisma参数,然后调用你这个函数。这是库与你的Prisma查询逻辑交互的核心

  2. countFn: () => Promise<number> | { select?: object }用于获取满足当前筛选条件(where)的总记录数。它可以是一个返回数字Promise的简单函数,也可以是一个Prismacount操作的配置对象(例如{ select: { _all: true } })。如果你不需要totalCount,可以传入() => Promise.resolve(0)

  3. connectionArgs: ConnectionArguments标准的Relay连接参数对象,包含:

    • first: 从头部开始取多少条记录。
    • after: 在某个游标之后开始取记录。
    • last: 从尾部开始取多少条记录。
    • before: 在某个游标之前开始取记录。 通常,你会从GraphQL Resolver的args中直接获取这个对象。
  4. opts: ConnectionOptions(可选)这是发挥库强大威力的地方,包含一系列精细化的配置:

    • getCursor?: (record: T) => Cursor:自定义如何从记录中提取游标。默认使用orderBy字段的值。
    • encodeCursor?: (cursor: Cursor) => string:自定义游标编码函数。默认使用Base64。
    • decodeCursor?: (cursorString: string) => Cursor:自定义游标解码函数。
    • recordToEdge?: (record: T) => Edge<T>:自定义如何将一条记录转换为Edge对象。
    • defaultSize?: number:当未指定firstlast时的默认分页大小。强烈建议始终设置此值,避免客户端意外请求过多数据
    • maxSize?: number:允许的最大分页大小,用于防止DoS攻击。

3.2 返回的连接对象结构

函数返回的result是一个ConnectionResult<T>类型的对象,其结构严格对应Relay规范:

{ edges: Array<{ node: T; // 你的Prisma模型数据 cursor: string; // 该节点的Base64编码游标 }>; pageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string | null; endCursor: string | null; }; totalCount: number; // 满足条件的总记录数 }

这个对象可以直接作为你的GraphQL Resolver的返回值。pageInfo中的布尔值由库根据是否还能向前/向后查询到数据来精确计算,这比手动判断要可靠得多。

3.3 高级配置场景实战

场景一:复合排序键作为游标默认情况下,库使用orderBy中的第一个字段作为游标。但如果你的排序是orderBy: [{ createdAt: 'desc' }, { id: 'asc' }],你需要一个复合游标。

const result = await findManyCursorConnection( (args) => prisma.post.findMany(args), () => prisma.post.count(), connectionArgs, { getCursor: (record) => ({ createdAt: record.createdAt, id: record.id }), encodeCursor: (cursor) => Buffer.from(JSON.stringify(cursor)).toString('base64'), decodeCursor: (cursorStr) => JSON.parse(Buffer.from(cursorStr, 'base64').toString()), } );

这里,游标被编码为一个包含createdAtid的JSON对象的Base64字符串。这确保了在createdAt相同的情况下,id能作为唯一的决胜字段。

场景二:自定义Edge内容有时,你可能想在Edge里附加一些额外信息,比如节点在列表中的本地计算属性。

const result = await findManyCursorConnection( (args) => prisma.post.findMany({ ...args, include: { author: true } }), () => prisma.post.count(), connectionArgs, { recordToEdge: (record) => ({ node: record, cursor: encodeCursor({ id: record.id }), // 使用你自己的编码函数 // 添加自定义字段(需在GraphQL Schema的Edge类型中定义) customField: `Posted by ${record.author.name}`, }), } );

注意,recordToEdge返回的对象会直接成为edges数组的一项。你需要确保GraphQL Schema中的PostEdge类型包含customField字段。

场景三:性能优化与安全限制在生产环境中,必须设置分页大小限制。

const DEFAULT_PAGE_SIZE = 20; const MAX_PAGE_SIZE = 100; const result = await findManyCursorConnection( findManyFn, countFn, connectionArgs, { defaultSize: DEFAULT_PAGE_SIZE, maxSize: MAX_PAGE_SIZE, } );

这样,如果客户端没有指定firstlast,则返回defaultSize条记录。如果客户端请求的数量超过maxSize,库会自动将其钳制(clamp)到maxSize。这是一个非常重要的安全性和性能保护措施。

4. 完整集成与实操指南

理论说再多,不如动手搭一个。下面我将带你从一个干净的Node.js项目开始,一步步构建一个支持Relay分页的GraphQL API。

4.1 项目初始化与依赖安装

首先,创建一个新目录并初始化项目,安装必要的依赖。

mkdir prisma-relay-demo cd prisma-relay-demo npm init -y npm install typescript ts-node @types/node --save-dev npm install prisma @prisma/client npm install @devoxa/prisma-relay-cursor-connection npm install graphql apollo-server

初始化TypeScript和Prisma:

npx tsc --init npx prisma init --datasource-provider sqlite # 这里用SQLite便于演示

编辑生成的prisma/schema.prisma文件,定义一个简单的博客模型:

generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) authorId Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([authorId]) }

运行迁移并生成Prisma客户端:

npx prisma migrate dev --name init npx prisma generate

4.2 构建GraphQL Schema与类型定义

创建schema.ts文件,定义GraphQL类型。注意,Connection和Edge类型需要手动定义

// schema.ts import { gql } from 'apollo-server'; export const typeDefs = gql` type Post { id: ID! title: String! content: String published: Boolean! authorId: Int! createdAt: String! updatedAt: String! } # 必须手动定义Edge和Connection类型 type PostEdge { node: Post! cursor: String! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Query { # Relay风格的分页查询 posts( first: Int after: String last: Int before: String where: PostWhereInput ): PostConnection! } input PostWhereInput { published: Boolean authorId: Int } `;

4.3 实现Resolver并集成库

创建resolver.ts文件。这里是核心,我们将在这里调用findManyCursorConnection

// resolver.ts import { PrismaClient } from '@prisma/client'; import { findManyCursorConnection } from '@devoxa/prisma-relay-cursor-connection'; import { ConnectionArguments } from '@devoxa/prisma-relay-cursor-connection/dist/types'; const prisma = new PrismaClient(); // 一个简单的游标编码/解码函数,使用默认的Base64 JSON格式 const encodeCursor = (cursor: any): string => { if (typeof cursor === 'number' || typeof cursor === 'string') { cursor = { id: cursor }; // 假设默认使用id字段 } return Buffer.from(JSON.stringify(cursor)).toString('base64'); }; const decodeCursor = (cursorString: string): any => { return JSON.parse(Buffer.from(cursorString, 'base64').toString()); }; export const resolvers = { Query: { posts: async (_, args: { first?: number; after?: string; last?: number; before?: string; where?: any }) => { const { where, ...connectionArgs } = args; // 构建Prisma的where条件 const prismaWhere = where || {}; // 调用库的核心函数 const result = await findManyCursorConnection( // findManyFn (findManyArgs) => { // 库会传入计算好的cursor, take, skip等参数,我们合并自定义的where和orderBy return prisma.post.findMany({ ...findManyArgs, where: prismaWhere, orderBy: { id: 'asc' }, // 指定排序字段,游标将基于此字段 }); }, // countFn () => prisma.post.count({ where: prismaWhere }), // connectionArgs connectionArgs as ConnectionArguments, // opts { defaultSize: 20, // 默认每页20条 maxSize: 100, // 最大每页100条 // 使用自定义的编码/解码,确保与可能的客户端解码方式一致 encodeCursor: (cursor) => encodeCursor(cursor), decodeCursor: (cursorString) => decodeCursor(cursorString), } ); return result; }, }, };

4.4 启动服务器与测试查询

创建index.ts作为服务器入口。

// index.ts import { ApolloServer } from 'apollo-server'; import { typeDefs } from './schema'; import { resolvers } from './resolver'; const server = new ApolloServer({ typeDefs, resolvers, }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); });

package.json中添加启动脚本:

"scripts": { "start": "ts-node index.ts", "seed": "ts-node seed.ts" }

创建一个seed.ts文件来生成一些测试数据:

// seed.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { await prisma.post.deleteMany({}); // 清空数据 const posts = Array.from({ length: 50 }, (_, i) => ({ title: `Post ${i + 1}`, content: `This is the content of post ${i + 1}.`, published: Math.random() > 0.5, authorId: Math.floor(Math.random() * 5) + 1, })); await prisma.post.createMany({ data: posts }); console.log('Seeded 50 posts.'); } main() .catch(e => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });

运行npm run seed生成数据,然后npm start启动服务器。

打开GraphQL Playground(通常是http://localhost:4000),执行以下查询:

query { posts(first: 5) { totalCount edges { cursor node { id title published } } pageInfo { hasNextPage hasPreviousPage endCursor } } }

你将得到一个标准的Relay连接响应。复制endCursor,在下一个查询中使用它:

query { posts(first: 5, after: "你的endCursor值") { edges { node { id title } } pageInfo { hasNextPage endCursor } } }

至此,一个完整的、支持Relay游标分页的GraphQL API后端就搭建成功了。前端Relay客户端现在可以无缝消费这个API。

5. 常见问题、性能考量与避坑指南

在实际使用中,你肯定会遇到一些疑问和挑战。下面是我在多个项目中总结出来的经验。

5.1 性能瓶颈与优化策略

问题1:totalCount查询在数据量大时很慢每次分页都执行一次count(*)在百万级数据表上是不可接受的。

解决方案

  • 场景A:不需要精确总数。如果你的UI只是显示“加载更多”,而不显示“共1000条,第5页”,那么完全可以省略totalCount,或者在首次请求后缓存它。将countFn设为() => Promise.resolve(0)
  • 场景B:需要精确总数,但可接受轻微延迟。使用缓存策略,例如将总数缓存到Redis中,并设置一个较短的过期时间(如30秒)。在countFn中先查缓存,未命中再查数据库并更新缓存。
  • 场景C:海量数据且必须精确。考虑使用估算(如PostgreSQL的reltuples)或分页时不返回总数,提供独立的totalPosts查询字段。

问题2:深分页性能问题即使用游标分页,当after的游标指向非常靠后的位置时,Prisma仍然需要扫描并跳过大量记录才能找到起点。

解决方案

  • 确保游标字段有索引orderBy使用的字段必须有数据库索引。对于复合排序(createdAt, id),需要创建复合索引CREATE INDEX idx_posts_created_at_id ON posts(created_at, id)
  • 限制分页深度:在业务层面限制用户只能翻阅前N页(例如100页)。可以通过检查游标解码后的值(如时间戳)来判断是否过深。
  • 使用更优的游标:如果可能,使用天生有序且分布均匀的字段作为游标,如自增ID或雪花算法生成的ID,避免使用频繁更新的字段。

5.2 排序、筛选与游标的兼容性

问题:筛选(where)条件改变后,游标失效这是游标分页的一个经典问题。如果第一页查询where: { published: true },得到的endCursor是ID=10。当第二页用这个游标查询时,如果数据发生了变化(例如ID=11的记录被设置为published: false),那么第二页的结果可能会少一条记录,或者出现意外偏移。

理解与应对: 这不是库的bug,而是游标分页的特性。游标分页保证的是在查询执行瞬间,基于当前数据集和排序规则的稳定遍历。它不保证不同查询之间数据的一致性。如果你的应用对一致性要求极高,需要考虑:

  1. 使用事务隔离级别为“可重复读”(Repeatable Read)来保证整个分页过程中的数据视图一致。
  2. 在业务设计上,避免在用户分页浏览过程中频繁修改正在被筛选的数据。
  3. 告知产品经理和前端同学,这是一种预期行为,并非错误。

问题:多字段排序与游标编码如前所述,复合排序需要复合游标。务必在getCursor函数中返回所有排序字段的值,并确保encodeCursordecodeCursor函数是互逆的。一个常见的错误是只编码了主排序字段,导致在次排序字段值相同时分页错乱。

5.3 错误处理与边界情况

firstlast同时传递: Relay规范不允许同时指定firstlast。库内部会进行校验,通常会导致错误。确保你的GraphQL层(通过Schema验证)或业务逻辑层提前阻止这种情况。

游标无效或不存在: 如果客户端传递了一个无效的或指向已删除记录的after游标,库会尝试解码它,并将其传递给Prisma的cursor参数。Prisma可能会找不到记录,从而返回一个空集或从开头开始查询(取决于游标值)。库不会主动验证游标的有效性。为了更好的用户体验,你可以在Resolver中添加逻辑:解码游标后,先查询一次该游标对应的记录是否存在。

const decodedCursor = decodeCursor(after); const cursorRecord = await prisma.post.findUnique({ where: { id: decodedCursor.id } }); if (!cursorRecord) { throw new UserInputError('提供的游标指向不存在的记录'); }

空数据集: 当没有数据时,库会返回{ edges: [], pageInfo: { hasNextPage: false, hasPreviousPage: false, ... }, totalCount: 0 }pageInfo中的游标为null。前端需要能正确处理这种情况。

5.4 与Prisma新特性的兼容性

Prisma在不断更新。prisma-relay-cursor-connection库的核心是调用你提供的findManyFn。只要Prisma的findMany参数类型FindManyArgs保持稳定,库就能正常工作。这意味着它天然兼容Prisma的include(关联加载)、select(字段选择)等所有功能。

例如,分页查询并同时加载关联的作者信息:

const result = await findManyCursorConnection( (args) => prisma.post.findMany({ ...args, include: { author: true }, // 关联查询 orderBy: { createdAt: 'desc' }, }), countFn, connectionArgs, opts ); // result.edges[0].node 现在将包含 `author` 对象

6. 进阶应用与模式扩展

掌握了基础用法和解决了常见问题后,我们可以看看一些更高级的应用模式。

6.1 构建可复用的分页函数

为了避免在每个Resolver中重复编写类似的逻辑,可以抽象一个工厂函数。

// lib/connectionHelpers.ts import { findManyCursorConnection, ConnectionOptions } from '@devoxa/prisma-relay-cursor-connection'; import { PrismaClient, Prisma } from '@prisma/client'; type ModelDelegate = { findMany: (args: any) => Promise<any[]>; count: (args: any) => Promise<number>; }; export function createConnectionResolver<T, M extends ModelDelegate>( modelDelegate: M, defaultOrderBy: Prisma.Args<M, 'findMany'>['orderBy'], baseOptions: Partial<ConnectionOptions<T>> = {} ) { return async ( connectionArgs: any, where?: Prisma.Args<M, 'findMany'>['where'], customOptions: Partial<ConnectionOptions<T>> = {} ) => { const opts: ConnectionOptions<T> = { defaultSize: 20, maxSize: 100, encodeCursor: (cursor) => Buffer.from(JSON.stringify(cursor)).toString('base64'), decodeCursor: (cursorStr) => JSON.parse(Buffer.from(cursorStr, 'base64').toString()), ...baseOptions, ...customOptions, }; return findManyCursorConnection( (args) => modelDelegate.findMany({ ...args, where, orderBy: defaultOrderBy }), () => modelDelegate.count({ where }), connectionArgs, opts ); }; } // 使用示例:在resolver中 import { prisma } from '../db'; import { createConnectionResolver } from '../lib/connectionHelpers'; const getPostConnection = createConnectionResolver( prisma.post, { id: 'asc' } // 默认排序 ); export const resolvers = { Query: { posts: (_, args) => getPostConnection(args, { published: true }), // 可以传入额外的where条件 }, };

6.2 处理复杂的嵌套连接查询

有时你需要对关联模型进行分页,例如“查询某个作者的所有文章”。这需要稍微调整一下思路。

假设Schema扩展了Author和Post的关联:

type Author { id: ID! name: String! posts(first: Int, after: String, last: Int, before: String): PostConnection! }

在Resolver中,你需要确保where条件包含了关联关系。

// resolver.ts const authorResolvers = { Author: { posts: async (parent, args) => { // parent 是 Author 对象,包含 id return findManyCursorConnection( (findManyArgs) => prisma.post.findMany({ ...findManyArgs, where: { authorId: parent.id }, // 关键:筛选出属于当前作者的文章 orderBy: { createdAt: 'desc' }, }), () => prisma.post.count({ where: { authorId: parent.id } }), args, { defaultSize: 10 } ); }, }, };

6.3 与GraphQL Code Generator或Nexus的集成

如果你使用像GraphQL Code Generator这样的工具来自动生成TypeScript类型,或者使用Nexus、TypeGraphQL等Schema-first/Code-first框架,集成同样顺畅。

以GraphQL Code Generator为例,你定义了Schema后,它会生成对应的TypeScript类型,如PostsQueryVariables,PostConnection等。你的Resolver函数只需要返回正确的类型即可,findManyCursorConnection的返回类型可以通过泛型或类型断言来匹配。

import { PostConnectionResolvers } from './generated/graphql'; const postResolvers: PostConnectionResolvers = { // ... 其他字段解析 posts: async (parent, args, context) => { const result = await findManyCursorConnection(...); // result 的类型需要断言或适配成生成的 PostConnection 类型 // 通常结构完全一致,可以直接返回 return result as any; // 或在创建时使用泛型 findManyCursorConnection<Post> }, };

对于Nexus或TypeGraphQL,你需要在定义ObjectType时明确字段的类型,然后在Resolver内部使用这个库来获取数据。逻辑与上述纯Apollo Server的示例本质相同。

6.4 测试策略

测试分页逻辑至关重要。你需要测试:

  1. 基础功能:无游标时返回第一页数据,pageInfo正确。
  2. 向前分页:使用after游标能正确获取下一页。
  3. 向后分页:使用before游标能正确获取上一页(如果支持)。
  4. 边界情况:请求数量超过maxSize时被钳制,请求最后一页时hasNextPage为false。
  5. 筛选条件:结合where参数时,分页和总数计算依然正确。

可以使用Jest等测试框架,配合一个内存数据库(如SQLite)或Prisma的mockDeep来进行单元测试和集成测试。重点验证返回的edges顺序、cursor的唯一性与连续性、以及pageInfo的准确性。

7. 总结与最终建议

经过以上从原理到实践,从基础到进阶的梳理,devoxa/prisma-relay-cursor-connection这个库的价值已经非常清晰:它用极简的API,干净地解决了Prisma与Relay之间数据格式的适配问题,让开发者能从繁琐的样板代码中解脱出来。

我个人最欣赏它的两点是:一是职责单一,它只做转换,不越界;二是配置灵活,通过opts参数暴露了足够的扩展点,能应对复合游标、自定义Edge等复杂场景。

最后,给打算在项目中引入它的朋友几点实在的建议:

一定要设置defaultSizemaxSize。这是保护你数据库和API的第一道防线,防止客户端误传或恶意传递一个巨大的first值(比如first: 1000000)导致服务雪崩。

深入理解游标分页的局限性,特别是与动态筛选结合时可能出现的“数据漂移”问题。在技术方案评审时,就需要和团队明确这一点,避免后期被认为是Bug。

对于简单项目,手动转换也许更直接。如果你的分页需求非常简单(只有firstafter,排序固定),且不打算支持Relay以外的客户端,那么自己写一个简单的转换函数可能也就二三十行代码。引入一个库也需要权衡依赖成本。

将分页逻辑封装起来。就像上面示例的createConnectionResolver,即使一开始只有一个地方用,也建议抽象一下。当第二个、第三个需要分页的Resolver出现时,你会感谢自己的这个决定。这不仅减少了重复代码,更保证了分页行为(如默认分页大小、游标编码方式)在整个应用中的一致性。

这个库就像一颗精准的齿轮,在Prisma和Relay这两台强大的机器之间起到了完美的连接作用。正确安装和使用它,能让你的GraphQL API在分页这个关键特性上运转得更加平稳、高效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询