You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 16:10:44