DGS/GraphQL查询中非空属性的服务端处理方案咨询
Netflix DGS中未请求非空关联字段的优化处理
问题背景
团队采用Netflix DGS + Spring Boot构建GraphQL API,定义了如下GraphQL模式:
type LegalEntity { id: UUID! name: String # 其他属性(省略) } type Contract { id: UUID! startDate: Date! endDate: Date licensor: LegalEntity! # 其他属性(省略) }
通过DGS代码生成工具得到Kotlin数据类:
public data class LegalEntity( @JsonProperty("id") public val id: String, @JsonProperty("name") public val name: String? = null ) { public companion object } public data class Contract( @JsonProperty("id") public val id: String, @JsonProperty("startDate") public val startDate: LocalDate, @JsonProperty("endDate") public val endDate: LocalDate? = null, @JsonProperty("licensor") public val licensor: LegalEntity ) { public companion object }
核心疑问
当收到未请求Contract.licensor字段的查询时:
query contract($id: UUID!) { contract(id: $id) { id startDate endDate } }
不想为了满足Kotlin非空属性要求而强制加载licensor(造成资源浪费,且前端会丢弃该数据),该如何处理?
解决方案
1. 字段级DataFetcher分离加载逻辑
这是DGS的标准最佳实践,完全贴合GraphQL按需加载的设计理念:
- 先实现
Contract根字段的DataFetcher,只加载id、startDate、endDate这些基础字段,不加载licensor。此时可为Contract类的licensor属性传入临时占位符(比如LegalEntity(id = contractDto.licensorId)),因为该字段不会被序列化返回。 - 单独为
Contract.licensor字段实现DataFetcher,仅当查询包含该字段时才会执行加载:
@DgsComponent class ContractDataFetchers { // 根字段Contract的DataFetcher,仅加载基础字段 @DgsData(parentType = "Query", field = "contract") fun getContract(@InputArgument("id") id: String): Contract { val contractDto = contractRepository.findById(id) return Contract( id = contractDto.id, startDate = contractDto.startDate, endDate = contractDto.endDate, // 临时占位符,仅用于满足非空类型要求,不会被返回 licensor = LegalEntity(id = contractDto.licensorId) ) } // Contract.licensor字段的DataFetcher,按需触发加载 @DgsData(parentType = "Contract", field = "licensor") fun getLicensor(dfe: DataFetchingEnvironment): LegalEntity { val contract = dfe.getSource<Contract>() // 可结合DataLoader实现批量加载,进一步优化性能 return legalEntityRepository.findById(contract.licensor.id) } }
2. 调整代码生成配置,使用延迟加载类型
修改DGS代码生成插件配置,将关联字段生成Lazy<LegalEntity>类型,实现延迟初始化:
- Maven插件配置示例:
<plugin> <groupId>com.netflix.graphql.dgs</groupId> <artifactId>dgs-codegen-maven-plugin</artifactId> <version>${dgs-codegen.version}</version> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <schemaPaths> <path>${project.basedir}/src/main/resources/schema/schema.graphqls</path> </schemaPaths> <packageName>com.yourpackage.generated</packageName> <!-- 启用延迟加载关联字段 --> <generateLazyTypes>true</generateLazyTypes> </configuration> </execution> </executions> </plugin>
生成后的Contract类会将licensor改为延迟加载类型:
public data class Contract( // ...其他属性 @JsonProperty("licensor") public val licensor: Lazy<LegalEntity> )
初始化时用lazy { ... }包裹加载逻辑,只有当访问licensor.value时才会执行数据加载,结合DGS的字段加载机制,仅在字段被请求时触发。
3. 使用投影类返回部分字段
启用DGS的投影类生成功能,返回仅包含请求字段的投影对象,无需处理未请求的非空属性:
- 在代码生成配置中添加
<generateProjections>true</generateProjections>,会生成对应类型的投影类(如ContractProjection)。 - 根字段DataFetcher中返回投影对象:
@DgsData(parentType = "Query", field = "contract") fun getContract(@InputArgument("id") id: String): ContractProjection { val contractDto = contractRepository.findById(id) return ContractProjection() .id(contractDto.id) .startDate(contractDto.startDate) .endDate(contractDto.endDate) // 不设置licensor,因为查询未请求 }
总结
优先选择字段级DataFetcher分离的方案,这是GraphQL按需加载的核心实现方式,也是DGS官方推荐的最佳实践,既能避免不必要的资源消耗,又能严格贴合业务需求。
内容的提问来源于stack exchange,提问作者Matheus Moreira
相关产品推荐
相关产品推荐

