【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
本文基于 How to GraphQL 教程仓库中content/backend/typescript-apollo/8-filtering-pagination-and-sorting.md一篇章节展开,完整讲解如何为基于 TypeScript、Apollo Server、Nexus 与 Prisma 构建的 HackerNews 克隆项目feed查询添加过滤(filtering)、Limit-Offset 分页(skip/take)、多字段排序(orderBy)以及返回总数(count)的完整能力。读完后你可以掌握:如何用 Nexus 的stringArg/intArg/inputObjectType/enumType构建类型安全的查询参数,如何把 Prisma Client 的where、skip、take、orderBy、count选项正确透传给findMany,以及为什么count与分页后返回的links数量会不一致这类设计细节。
一、本章目标:让 feed 查询支持过滤、分页与排序
在这一系列的上一章,你已经通过PrismaClient把feed查询接入了真实的 SQLite 数据库(参见 连接服务器与数据库 中的context.prisma.link.findMany())。本章的目标是让客户端能够约束feed查询返回的Link列表:
- 过滤:客户端提供一个过滤字符串,只返回
url或description中包含该子串的Link; - 分页:客户端提供
skip(偏移量)与take(条数)两个参数,按 Limit-Offset 模型分页; - 排序:客户端声明一个或多个排序条件(字段 + 升/降序);
- 总数:
feed不再直接返回列表,而是返回一个包含links、count、id的Feed对象。
整个实现思路与系列前几章一致:查询解析的重活由 Prisma 完成,Nexus 只负责把 GraphQL 参数“透传”给context.prisma.link.findMany(...)。
二、过滤:新增可选的 filter 参数
使用PrismaClient后,实现过滤并不需要多少代码。关键设计决策是:feed查询接受一个过滤字符串filter,只返回url或description中包含该子串的Link(两者满足其一即可,即 OR 语义)。
在src/graphql/Link.ts中为feed查询添加新的字符串类型参数filter,并更新 resolver:
export const LinkQuery = extendType({ type: "Query", definition(t) { t.nonNull.list.nonNull.field("feed", { type: "Link", args: { filter: stringArg(), // 1 }, resolve(parent, args, context) { const where = args.filter // 2 ? { OR: [ { description: { contains: args.filter } }, { url: { contains: args.filter } }, ], } : {}; return context.prisma.link.findMany({ where, }); }, }); }, });注释解读:
// 1:注意filter参数是可选的(可选参数是stringArg()默认行为),客户端可以省略它来跳过过滤;// 2:如果提供了filter参数,就构造一个表达过滤条件的where对象——“description或url(或两者)中包含与过滤字符串匹配的子串”。这个where参数被 Prisma 用来筛掉不符合条件的Link元素;如果没有提供filter,where就是空对象,行为与之前完全一致。
变更之后,Nexus 重新生成(npm run generate)得到的 GraphQL schema 中,feed查询变为:
type Query { feed(filter: String): [Link!]! }可以用如下查询测试过滤功能:
query { feed(filter: "nexus") { id description url postedBy { id name } } }预期返回类似:
{ "data": { "feed": [ { "id": 1, "description": "Code-First GraphQL schemas for JavaScript/TypeScript", "url": "nexusjs.org", "postedBy": { "id": 1, "name": "alice" } } ] } }建议多试几个过滤字符串。注意:如果提供了一个不匹配任何link的过滤条件,会收到一个空数组([]),而不是报错——这是过滤语义的自然结果。
三、分页:Limit-Offset 模型与 skip / take 参数
3.1 两种主流分页模型
分页是 API 设计中的经典难题。从高层看,主要有两种思路:
- Limit-Offset(限制-偏移):通过提供要取回元素的索引(实际上是起始索引offset和要取回的元素个数limit)来请求列表中的特定“块”;
- Cursor-based(游标式):更进阶的模型。列表中每个元素都关联一个唯一 ID(即cursor),分页的客户端提供起始元素的游标以及要取回的元素个数。
Prisma 同时支持两种分页方式。本教程选择实现Limit-Offset 分页,因为它与数据库的“跳过 + 取 N 条”能力直接对应,实现成本最低。
3.2 术语对应:limit 是 take,offset 是 skip
Limit 和 offset 在 Prisma API 中有不同的名字:
- limit叫
take——从给定起始索引开始“取”(take)x个元素; - offset(起始索引)叫
skip——先“跳过”(skip)列表里那么多元素,再收集要返回的项目。如果未提供skip,其默认值为0,分页总是从列表开头开始。
因此,给feed查询添加skip与take两个参数,并相应更新 resolver:
import { extendType, idArg, nonNull, objectType, stringArg, intArg } from "nexus"; export const LinkQuery = extendType({ type: "Query", definition(t) { t.nonNull.list.nonNull.field("feed", { type: "Link", args: { filter: stringArg(), skip: intArg(), // 1 take: intArg(), // 1 }, resolve(parent, args, context) { const where = args.filter ? { OR: [ { description: { contains: args.filter } }, { url: { contains: args.filter } }, ], } : {}; return context.prisma.link.findMany({ where, skip: args?.skip as number | undefined, // 2 take: args?.take as number | undefined, // 2 }); }, }); }, });变更细节:
// 1:skip和take都是可选的整型参数,分别代表 offset 与 limit;// 2:Prisma Client API 会把skip与take作为findMany查询的附加选项,据此返回link记录。若任一参数缺失,就向 Prisma 传undefined。这里有一个类型不匹配问题:Nexus 生成的参数类型是number | undefined | null,而 Prisma 期望的是number | undefined,因此需要as number | undefined做类型断言,剥掉null这一分支。
注意:在 JavaScript 和 TypeScript 中,
undefined与null经常被混用;但 Prisma 严格区分二者——在 Prisma 中,null是一个具体的_值_,而undefined表示“什么都不做”/忽略该选项。这是为什么透传时要统一使用undefined而非null的原因。
更新后,schema 中的feed查询变为:
type Query { feed(filter: String, skip: Int, take: Int): [Link!]! }可以用下面的查询测试分页 API,它返回列表中的第二个Link:
query { feed(take: 1, skip: 1) { id description url } }预期返回:
{ "data": { "feed": [ { "id": 2, "description": "Next-generation Node.js and TypeScript ORM", "url": "www.prisma.io" } ] } }四、排序:LinkOrderByInput 输入类型与 Sort 枚举
4.1 定义排序选项类型
Prisma 允许按特定标准返回排序(ordered)的元素列表。例如可以按url或description字母序排列Link列表,且支持升序(asc)与降序(desc)。对 HackerNews API,教程把“如何排序”完全交给客户端决定,因此把 Prisma API 的全部排序选项都暴露到 GraphQL API 中——做法是创建一个input类型(LinkOrderByInput)和一个枚举(Sort):
import { extendType, nonNull, objectType, stringArg, intArg, inputObjectType, enumType, arg } from "nexus"; export const LinkOrderByInput = inputObjectType({ name: "LinkOrderByInput", definition(t) { t.field("description", { type: Sort }); t.field("url", { type: Sort }); t.field("createdAt", { type: Sort }); }, }); export const Sort = enumType({ name: "Sort", members: ["asc", "desc"], });这会在 GraphQL schema 中生成以下类型:
input LinkOrderByInput { createdAt: Sort description: Sort url: Sort } enum Sort { asc desc }LinkOrderByInput表示列表可排序的标准(字段),Sort枚举定义排序方向。三个可排序字段description、url、createdAt恰好对应数据库Link模型中的业务字段(id为自增主键,一般不开放给用户排序)。
4.2 为 feed 添加 orderBy 参数
在feed查询中新增orderBy参数并更新 resolver:
import { extendType, nonNull, objectType, stringArg, intArg, inputObjectType, enumType, arg, list } from "nexus"; import { Prisma } from "@prisma/client" export const LinkQuery = extendType({ type: "Query", definition(t) { t.nonNull.list.nonNull.field("feed", { type: "Link", args: { filter: stringArg(), skip: intArg(), take: intArg(), orderBy: arg({ type: list(nonNull(LinkOrderByInput)) }), // 1 }, resolve(parent, args, context) { const where = args.filter ? { OR: [ { description: { contains: args.filter } }, { url: { contains: args.filter } }, ], } : {}; return context.prisma.link.findMany({ where, skip: args?.skip as number | undefined, take: args?.take as number | undefined, orderBy: args?.orderBy as Prisma.Enumerable<Prisma.LinkOrderByWithRelationInput> | undefined, // 2 }); }, }); }, });两处变更:
// 1:新的orderBy参数是LinkOrderByInput输入类型的数组。在其中可以提供一个或多个排序标准(createdAt、description、url),并指定排序方向(asc或desc),feed 中的链接将按此排序。按这个设计,通过传入多个LinkOrderByInput实例可以实现多字段排序(例如先按url排,url相同时再按createdAt排);// 2:传给 Prisma 的orderBy选项与前面的skip/take类似,由于 Nexus 生成的类型含null分支而 Prisma 期望Prisma.Enumerable<Prisma.LinkOrderByWithRelationInput> | undefined,因此同样需要类型断言剥离null选项。
用下面的查询测试按创建时间倒序排序:
query { feed(orderBy: [{ createdAt: desc }]) { id createdAt description url } }结果类似:
{ "data": { "feed": [ { "id": 3, "createdAt": "2021-12-15T04:20:33.616Z", "description": "Next-generation Node.js and TypeScript ORM", "url": "www.prisma.io" }, { "id": 1, "createdAt": "2021-12-14T23:21:52.620Z", "description": "Code-First GraphQL schemas for JavaScript/TypeScript", "url": "nexusjs.org" } ] } }建议:到此为止,可以再加几条 link 记录,尝试多字段排序(如
orderBy: [{ url: asc }, { createdAt: desc }]);另外把排序、过滤、分页组合起来(filter+skip/take+orderBy)实验一下,观察结果。注意三者的执行顺序是由 Prisma 决定的:where先筛选,orderBy再排序,skip/take最后截取分页块——所以“先过滤再分页”天然成立。
五、返回 Link 总数:重构 feed 为 Feed 对象
5.1 为什么需要 count
最后一个功能是让 API 能够回答“数据库里当前有多少条Link”。为此需要把feed查询重构一下:不再直接返回列表,而是返回一个新的类型Feed。动机在于:当使用take分页时,_返回_的 links 数量可能与数据库中_可用_的 links 数量不同,客户端需要一个独立的count字段来渲染分页器。
5.2 定义 Feed 类型
在Link.ts中创建新的Feed类型:
export const Feed = objectType({ name: "Feed", definition(t) { t.nonNull.list.nonNull.field("links", { type: Link }); // 1 t.nonNull.int("count"); // 2 t.id("id"); // 3 }, });各字段含义:
// 1:links是Link类型对象的数组,即当前feed查询的返回内容本身;// 2:count是整型,表示数据库中匹配 feed 查询条件的link数量。这一点非常重要:分页取回的数量不等于可用的总数量;// 3:id是ID类型字段,GraphQL 内置的唯一标识类型,序列化/反序列化方式与String相同。
生成后 schema 中的Feed类型为:
type Feed { count: Int! id: ID links: [Link!]! }5.3 调整 feed 查询签名与 resolver
export const LinkQuery = extendType({ type: "Query", definition(t) { t.nonNull.field("feed", { // 1 type: "Feed", args: { filter: stringArg(), skip: intArg(), take: intArg(), orderBy: arg({ type: list(nonNull(LinkOrderByInput)) }), }, async resolve(parent, args, context) { const where = args.filter ? { OR: [ { description: { contains: args.filter } }, { url: { contains: args.filter } }, ], } : {}; const links = await context.prisma.link.findMany({ where, skip: args?.skip as number | undefined, take: args?.take as number | undefined, orderBy: args?.orderBy as | Prisma.Enumerable<Prisma.LinkOrderByWithRelationInput> | undefined, }); const count = await context.prisma.link.count({ where }); // 2 const id = `main-feed:${JSON.stringify(args)}`; // 3 return { // 4 links, count, id, }; }, }); }, });变更点逐项说明:
// 1:feed查询的返回类型更新为单个非空的Feed实例(注意这里去掉了list,由t.nonNull.list.nonNull.field变为t.nonNull.field);// 2:使用 Prisma 的count API(prisma.link.count({ where }))返回匹配当前过滤条件的记录数。skip、take、orderBy对计算数量没有意义,因此这条查询里省略了它们——但where必须保留,保证 count 与 links 使用同一套过滤条件;// 3:通过把查询入参序列化后拼到标识符后缀上,为 feed 查询生成唯一的id:main-feed:${JSON.stringify(args)}。这样不同的参数组合(不同过滤/分页/排序)总会得到不同的、唯一的标识符,对基于id做缓存或订阅的前端缓存层(如 Apollo Client 的缓存键)十分友好;// 4:resolve函数返回的对象已更新为与Feed类型签名一致:{ links, count, id }。
注意 resolver 现在必须声明为async,因为内部有两次await(findMany与count)。
5.4 验证:count 与返回数量不一致是正常的
用如下查询测试更新后的feed:
query { feed (take: 1) { count links { id createdAt description } } }返回类似:
{ "data": { "feed": { "count": 2, "links": [ { "id": 1, "createdAt": "2021-12-14T23:21:52.620Z", "description": "Code-First GraphQL schemas for JavaScript/TypeScript" } ] } } }注意count与返回的 links 数量并不相等:take限制了_返回_的链接数量,但不影响count,后者反映的是数据库中_可用_(匹配过滤条件)的链接总数。这正是前端分页器需要的语义——“这一页取了 1 条,但一共有 2 条”。
六、小结与在本教程中的位置
这一章完成了 HackerNews API 中“健壮列表查询”的最后一块拼图。回顾feed查询的完整演进路径(每一步对应本系列的一个章节):
- 硬编码的内存数组(一个简单的查询);
- 接入 Prisma 的
link.findMany()(连接服务器与数据库); - 本章:
filter(OR 子串匹配where)→skip/take(Limit-Offset 分页)→orderBy(多字段、多方向排序)→ 重构为返回{ links, count, id }的Feed对象。
几个值得记住的工程要点:
- 可选参数与 Prisma 的 undefined 语义:所有列表参数(
filter、skip、take、orderBy)都是可选的,透传缺失值时统一使用undefined(“忽略该选项”),而不是null(一个具体值); - 类型断言的原因:Nexus 生成的参数类型带
| null分支,与 Prisma 期望的... | undefined不兼容,故需as断言收窄; - count 与分页解耦:
count查询只带where,不带skip/take/orderBy,保证统计的是过滤后全量数据; - 参数化 feed id:
main-feed:${JSON.stringify(args)}使不同参数组合拥有稳定且唯一的标识符,便于前端缓存区分。
在本教程仓库中,该后端能力与前端教程直接对接:React & Apollo 教程的入门章节 中展示的接口签名即为本章的最终形态——feed(filter: String, skip: Int, take: Int, orderBy: LinkOrderByInput): Feed!,前端使用feed(skip: 0, take: 10)这类查询消费过滤、分页与排序能力。后续章节(部署 与系列总结)将把这套 API 部署上线并回顾整个 TypeScript + Apollo + Prisma 的技术栈。
适用前提说明:本篇基于教程中
hackernews-typescript示例项目(依赖nexus@^1.1.0、apollo-server@^3.x、prisma@^3.5.0、SQLite 数据源,模型Link含id、createdAt、description、url字段,数据模型定义见 添加数据库章节 中的schema.prisma)。示例配套源码仓库未包含在本内容仓库中,文中所有src/graphql/Link.ts路径均相对于该示例项目根目录;迁移到其他项目时,字段名与模型需按自身schema.prisma调整。
【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
相关推荐
Prisma API 查询(Queries)完全指南:对象查询、Connection 分页与过滤排序实战
Prisma API 查询(Queries)完全指南:对象查询、Connection 分页与过滤排序实战 本指南以 Prisma 1.x 服务端 API 为对象
后端数据库GraphQLPrisma GraphQL API 查询(Queries)权威指南:对象查询、Connection、过滤与分页实战
Prisma GraphQL API 查询(Queries)权威指南:对象查询、Connection、过滤与分页实战 Prisma API 是 Prisma 服
后端数据库GraphQLPrisma 1 GraphQL API 查询指南:Object Queries、Connection Queries 与过滤/排序/分页全解析
Prisma 1 GraphQL API 查询指南:Object Queries、Connection Queries 与过滤/排序/分页全解析 导读 本文基于
后端数据库GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考