如何在分页Product对象中查询关联Station的Product维度revenue?
定义的GraphQL Schema
scalar Timestamp input StatsArgs{ start: Timestamp! end: Timestamp } input PaginationArgs{ page: Int! limit: Int! } type Station{ id: ID! name: String! revenue( statsArgs: StatsArgs! ): Float! } type Product{ id: ID! name: String! stations: [Station!]! } type PaginatedProducts{ pages: Int! total: Int! items: [Product!]! } type Shop{ id: ID! name: String! stations: [Station!]! products( paginationArgs: PaginationArgs! ): PaginatedProducts! } type Query{ shops( shopID: String ): [Shop!] } schema{ query: Query }
执行的查询语句
query { shops { products(paginationArgs: { page: 1, limit: 20 }) { id name stations { id name revenue(statsArgs: { start: "2023-01-01", end: "2023-01-31" }) } } } }
核心问题
使用gqlgen这类代码生成库时,无法高效获取Station关联对应Product的revenue数据,且不想采用以下两种方案:
- 方案1:在
productResolver.stations解析器中预获取所有Station的revenue,性能损耗大; - 方案2:新增Query字段
productStations,破坏Schema的优雅性且实用性低。
一、Schema调整方案
1. 给Station添加内部关联字段(不暴露给客户端)
在服务端的Station结构体中添加非导出的productID字段(客户端不可见),当productResolver.stations返回Station列表时,给每个Station实例赋值对应的Product ID。后续解析revenue字段时,直接用该ID结合StatsArgs查询关联数据。
gqlgen中的实现示例:
// 服务端内部使用的Station结构体,包含非导出的productID type Station struct { ID string `json:"id"` Name string `json:"name"` productID string // 仅服务端可访问,不暴露给GraphQL客户端 } // Station的revenue字段解析器 func (r *stationResolver) Revenue(ctx context.Context, obj *Station, statsArgs *StatsArgs) (float64, error) { // 利用obj.productID + Station.ID + statsArgs查询关联营收 return fetchProductStationRevenue(obj.productID, obj.ID, statsArgs.Start, statsArgs.End) }
这种方式仅在客户端请求revenue字段时才触发查询,避免了预加载的性能浪费。
2. 新增中间关联类型显式绑定Product与Station
调整Schema,新增ProductStation类型作为Product和Station的关联载体,将revenue字段移到该类型上:
type ProductStation { station: Station! revenue(statsArgs: StatsArgs!): Float! } type Product { id: ID! name: String! productStations: [ProductStation!]! }
对应的查询语句修改为:
query { shops { products(paginationArgs: { page: 1, limit: 20 }) { id name productStations { station { id, name } revenue(statsArgs: { start: "2023-01-01", end: "2023-01-31" }) } } } }
此时productResolver.productStations返回的ProductStation实例自带Product ID和Station ID,解析revenue时直接用这两个ID查询,逻辑清晰且无关联信息丢失问题。
二、Go无代码生成库方案
如果不想依赖gqlgen的代码生成机制,可以使用GraphQL-Go(github.com/graphql-go/graphql),纯代码定义Schema,灵活控制字段解析逻辑。
核心逻辑:在解析revenue字段时,从父节点(Product)中提取Product ID,结合Station ID和StatsArgs查询数据。
代码片段示例:
// 定义StatsArgs输入类型 statsArgsInputType := graphql.NewInputObject(graphql.InputObjectConfig{ Name: "StatsArgs", Fields: graphql.InputObjectConfigFieldMap{ "start": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, "end": &graphql.InputObjectFieldConfig{Type: graphql.String}, }, }) // 定义Station类型 stationType := graphql.NewObject(graphql.ObjectConfig{ Name: "Station", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.ID}, "name": &graphql.Field{Type: graphql.String}, "revenue": &graphql.Field{ Type: graphql.Float, Args: graphql.FieldConfigArgument{ "statsArgs": &graphql.ArgumentConfig{Type: graphql.NewNonNull(statsArgsInputType)}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { // 从父节点(ProductStation/Product)获取Product ID stationData := p.Source.(map[string]interface{}) productID := stationData["productID"].(string) stationID := stationData["id"].(string) statsArgs := p.Args["statsArgs"].(map[string]interface{}) // 查询关联营收 return fetchProductStationRevenue(productID, stationID, statsArgs["start"].(string), statsArgs["end"].(string)) }, }, }, }) // 定义Product类型 productType := graphql.NewObject(graphql.ObjectConfig{ Name: "Product", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.ID}, "name": &graphql.Field{Type: graphql.String}, "stations": &graphql.Field{ Type: graphql.NewList(stationType), Resolve: func(p graphql.ResolveParams) (interface{}, error) { productID := p.Source.(map[string]interface{})["id"].(string) // 查询该Product关联的Station,并附加Product ID stations, err := fetchStationsByProductID(productID) for i := range stations { stations[i]["productID"] = productID } return stations, err }, }, }, })
这种方式完全无需代码生成,所有解析逻辑由开发者自主控制,能轻松实现Product与Station的关联查询。
内容的提问来源于stack exchange,提问作者empjon

