示例说明
Relay 连接规范是 GraphQL 分页的事实标准:edges 承载数据与游标,pageInfo 声明续页信息。解析器把这两个字段从键序存储里组装出来——first 参数约束页大小(夹在 1 到 100 之间防御恶意大页),after 游标解码为时间锚点,过滤出严格晚于锚点的记录再截断。
技术要点
- first 参数夹在安全区间防深拉取
- 游标解码失败立即报错
- hasNextPage 由锚点后剩余记录判定
代码解读
Orders 的过滤循环跳过锚点之前的订单,凑满 limit 即止;hasNextPage 再扫一遍判断锚点之后是否还有记录——多取判定的替代实现。EndCursor 取本页末条的时间串。结构体的 json 标签与 GraphQL schema 字段一一对应,gqlgen 的模型绑定直接复用。
使用建议
游标可加密签名防客户端构造任意锚点。排序键与游标键必须一致且建索引。深层翻页建议换搜索或筛选交互。
代码示例
package main
// gqlgen resolver implementing Relay-style GraphQL cursor pagination
import (
"context"
"fmt"
"time"
)
type Order struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Total int `json:"total"`
}
type OrderEdge struct {
Cursor string `json:"cursor"`
Node Order `json:"node"`
}
type PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
EndCursor string `json:"endCursor"`
}
type OrderConnection struct {
Edges []OrderEdge `json:"edges"`
PageInfo PageInfo `json:"pageInfo"`
}
type Resolver struct {
orders []Order
}
func (r *Resolver) Orders(
ctx context.Context, first *int, after *string,
) (*OrderConnection, error) {
limit := 20
if first != nil && *first > 0 && *first <= 100 {
limit = *first
}
// 存储按时间倒序,游标即 createdAt 的 ISO 串
afterTime := time.Time{}
if after != nil {
parsed, err := time.Parse(time.RFC3339, *after)
if err != nil {
return nil, fmt.Errorf("invalid cursor: %w", err)
}
afterTime = parsed
}
edges := make([]OrderEdge, 0, limit)
for _, o := range r.orders {
if !afterTime.IsZero() && !o.CreatedAt.After(afterTime) {
continue // 跳过游标之前(含锚点)的记录
}
if len(edges) == limit {
break
}
edges = append(edges, OrderEdge{
Cursor: o.CreatedAt.Format(time.RFC3339),
Node: o,
})
}
hasNext := false
lastCursor := ""
if len(edges) > 0 {
last := edges[len(edges)-1].Node.CreatedAt
lastCursor = edges[len(edges)-1].Cursor
for _, o := range r.orders {
if o.CreatedAt.After(last) {
hasNext = true
break
}
}
}
return &OrderConnection{
Edges: edges,
PageInfo: PageInfo{HasNextPage: hasNext, EndCursor: lastCursor},
}, nil
}