Ohhnews

分类导航

$ cd ..
foojay原文

面向Java开发者的GraphQL:构建灵活的数据层

#graphql#java#spring#netflix dgs#mongodb

目录

GraphQL 基础为什么 GraphQL 非常适合 Spring 生态系统选择结合 Spring for GraphQL 的 Netflix DGS项目设置

多年来,REST 一直是 Java 生态系统中创建 API 的标准架构风格。诸如 Spring MVC 以及最近的 Spring WebFlux 等框架,使得在结构良好的服务层支持下,通过 REST 范式公开 HTTP 端点变得轻而易举。在许多情况下,这种模型运作良好,并成为众多企业解决方案的基础。

然而,随着应用程序的增长和前端需求变得更加动态,基于 REST 的 API 开始显露出其局限性。返回刚性 DTO(数据传输对象)的多个端点往往会导致过度获取、获取不足,以及为了满足略有不同的客户需求而激增的专用端点。随着时间的推移,API 的演进变得更加困难,除非进行彻底的变革。

GraphQL 从不同的角度处理这个问题。它不再公开一系列端点,而是公开一个定义可用数据和操作的强类型模式。客户端描述他们想要什么,服务器决定如何检索它。这种变化起初可能看起来很奇怪,但它与 Java 开发人员已经熟悉的概念出奇地契合:契约、类型安全和显式演进。

在本文中,我们将探讨如何使用 Spring for GraphQL、Netflix DGS 和 MongoDB 构建一个灵活的、可用于生产环境的 GraphQL 数据层。我们将重点关注设计决策、权衡、模式以及模型,这些对于 GraphQL API 超越实验阶段至关重要。

GraphQL 基础

从概念上讲,GraphQL 基于三个主要原则:

  • 模式即 API 契约
  • 查询和变更即操作
  • 解析器(数据检索器)即执行逻辑

对于 Java 开发人员来说,与 REST 最重要的区别在于,模式不仅仅是文档。它是可执行的。每个查询在到达 Java 代码之前都会根据模式进行检查。仅凭这一点就消除了在松散指定的 API 中常见的一整类运行时错误。

另一个重要的变化是 GraphQL API 是由客户端驱动的。服务器不再决定响应的确切形式。相反,它确保请求的字段是可用的且在形式上是有效的。这使得 API 更容易演进,但也给后端开发人员带来了更大的责任,需要仔细考虑性能和数据访问模式。

为什么 GraphQL 非常适合 Spring 生态系统

Spring 应用程序已经按照一些既定原则进行组织:控制反转、声明式配置和关注点分离。GraphQL 并没有取代这些想法,而是对它们进行了补充。

Spring for GraphQL 将 GraphQL 集成到 Spring 生态系统中,允许 GraphQL 执行利用以下优势:

  • 依赖注入。
  • 验证。
  • 安全过滤器。
  • 可观察性和指标。

解析器只是 Spring Bean,GraphQL 请求通过与系统其余部分相同的应用程序上下文传递。这意味着 GraphQL 不是一种外来技术,而是基于 Spring 的现有架构的自然扩展。

选择结合 Spring for GraphQL 的 Netflix DGS

Spring for GraphQL 是 GraphQL Java 和 Spring 生态系统之间的桥梁,它处理模式加载、请求执行,并在 Spring 内部自然地连接运行时。对于较简单的 GraphQL API,它可能就足够了。

然而,随着 API 的增长,其他因素开始发挥作用:模式生命周期管理、解析器组织、批处理、可测试性和长期演进。Netflix DGS 在这些方面发挥了重要作用。

DGS 基于 Spring for GraphQL,采用“优先模式”的方法,将 GraphQL 模式视为一等公民的产物,而不是从 Java 代码隐式派生出来的东西。这使得契约显式化、版本控制化,并随着时间的推移更容易安全地演进。

DGS 还对与数据检索相关的所有方面提供强有力的支持,包括批处理和缓存。虽然 DataLoader 在 Spring for GraphQL 中可用,但 DGS 提供了更清晰的约定和一等抽象,减少了 N+1 查询问题等性能问题的可能性。

从可维护性的角度来看,DGS 非常适合大型代码库。它在查询、变更和字段解析器之间的清晰分离,以及专门的测试工具和可选的代码生成,有助于在添加更多开发人员和客户端时保持 GraphQL API 的可管理性。

最后,DGS 在设计时考虑了联合 GraphQL 架构。即使联合架构不是当前的需求,选择一个不限制架构未来演进的框架通常是一个务实的决定。

项目设置

为了创建本文的原型,我们假设我们将使用:

  • Java 25。
  • Spring Boot 3.5。
  • MongoDB。
  • Maven。

依赖项

Netflix DGS 启动器包含了在 Spring Boot 上使用 GraphQL 所需的所有依赖项。我们还将使用 Spring Data 与 MongoDB 进行交互。

$ xml
<dependency>
  <groupId>com.netflix.graphql.dgs</groupId>
  <artifactId>graphql-dgs-spring-boot-starter</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

此时,应用程序已准备好加载 GraphQL 模式并对其执行查询和变更。

领域模型概览

我们将处理一个非常简单但真实的领域:

  • 用户
  • 订单
  • 产品

一个用户可以下多个订单,每个订单可以有多个产品。选择这个模型是因为它突出了 GraphQL 最重要的方面之一,即高效解析关系的能力。

定义 GraphQL 模式

在 Netflix DGS 中,模式是起点。

$ graphql
type Query {
  users: [User!]!
  userById(id: ID!): User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

type User {
  id: ID!
  email: String!
  name: String!
  orders: [Order!]!
}

type Order {
  id: ID!
  totalAmount: Float!
  products: [Product!]!
}

type Product {
  id: ID!
  name: String!
  price: Float!
}

input CreateUserInput {
  email: String!
  name: String!
}

需要强调一些设计选择:

  • 非空性是显式的且有意的。
  • 输入类型与输出类型是分开的。
  • 关系是模式的一部分,而不是事后补充。

该模式已经比大多数 REST 契约更清晰地传达了 API 期望。

使用 MongoDB 进行持久化

使用 Spring Data MongoDB 进行持久化是传统的处理方式。

$ java
@Document("users")
public class UserDocument {
    @Id
    private String id;
    private String email;
    private String name;
}

避免直接将持久化模型用作 GraphQL 类型是一种良好的做法。MongoDB 文档倾向于快速演进,并且通常包含不应公开的内部字段或非规范化数据。

专用的映射层保持了职责的分离,并使模式演进更加安全。

使用 Netflix DGS 进行查询解析

要将 GraphQL 查询转换为后端操作,您需要使用查询解析器。其职责是编排,而不是应用业务逻辑。

$ java
@DgsComponent

public class UserQueryResolver {
    private final UserRepository repository;
    public UserQueryResolver(UserRepository repository) {
        this.repository = repository;
    }

    @DgsQuery
    public List<User> users() {
        return repository.findAll()
                .stream()
                .map(UserMapper::toGraphQL)
                .toList();
    }
}

从这个片段中,我们注意到:

  • 解析器是一个简单的 Spring Bean。
  • 模式决定了 API 的结构,而不是方法签名。
  • 业务逻辑保留在服务层中。

保持解析器的精简使它们更易于测试和理解。

变更与输入验证

变更代表写操作,应像 REST POST 和 PUT 端点一样受到同等程度的重视。

$ java
@DgsComponent

public class UserMutationResolver {
    private final UserRepository repository;
    public UserMutationResolver(UserRepository repository) {
        this.repository = repository;
    }

    @DgsMutation
    public User createUser(@InputArgument CreateUserInput input) {
        UserDocument doc = new UserDocument(
            input.getEmail(),
            input.getName()
        );
        return UserMapper.toGraphQL(repository.save(doc));
    }
}

输入验证和验证可以通过以下方式处理:

  • Bean Validation 注解。
  • 解析器中的显式检查。
  • 自定义 GraphQL 错误映射。

GraphQL 支持部分失败,因此验证错误必须精确且易于管理。

在 MongoDB 中解析关系

与关系数据库不同,MongoDB 不支持连接操作。在 GraphQL 中,关系是逐字段延迟解析的。

$ java
@DgsComponent

public class UserFieldResolver {
    private final OrderRepository orderRepository;
    public UserFieldResolver(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @DgsData(parentType = "User", field = "orders")
    public List<Order> orders(DgsDataFetchingEnvironment env) {
        User user = env.getSource();
        return orderRepository.findByUserId(user.getId())
                .stream()
                .map(OrderMapper::toGraphQL)
                .toList();
    }
}

这种方法很强大,但它带有一个显著的缺点。

N+1 查询问题

这是 GraphQL 在演示中看起来很简单,但在生产环境中可能导致重大问题的案例之一。

像这样的查询...

$ graphql
{
  users {
    id
    orders {
      id
    }
  }
}

...很容易导致:

  • 查询用户。
  • 查询与用户关联的订单。

该模型需要考虑解决方案的可扩展性。

在 Netflix DGS 中使用 DataLoader

DataLoader 使 GraphQL 能够对在单个请求中检索的相关数据进行分组和缓存。

$ java
@DgsComponent

public class OrderDataLoader {
    @DgsDataLoader(name = "ordersByUser")
    public BatchLoader<String, List<Order>> ordersByUser(OrderRepository repo) {
        return userIds -> CompletableFuture.supplyAsync(() ->
            repo.findByUserIds(userIds)
        );
    }
}

然后,字段解析器变为异步的:

$ java
@DgsData(parentType = "User", field = "orders")

public CompletableFuture<List<Order>> orders(DgsDataFetchingEnvironment env) {
    DataLoader<String, List<Order>> loader =
        env.getDataLoader("ordersByUser");
    return loader.load(env.<User>getSource().getId());
}

这将 N 次数据库调用转换为一次分组查询,对于非平凡的模式应被视为强制性的。

GraphQL 中的错误处理

GraphQL 允许部分成功:一个字段可能会失败,而其他字段继续返回数据。这需要与 REST 范式不同的方法。

在实践中:

  • 将领域异常映射到 GraphQL 错误。
  • 避免对外传达内部细节。
  • 使用错误扩展来传递结构化元数据。

Netflix DGS 提供了钩子来一致地自定义错误处理。

安全考量

GraphQL 通过单个端点暴露了巨大的攻击面,这使得传统的基于端点的安全性变得不足。强大的安全策略通常结合多层防护:

  • HTTP 级身份验证,由 Spring Security 处理(JWT, OAuth2)
  • 方法级授权,应用于解析器或服务
  • 针对敏感数据的字段级限制
  • 查询深度和复杂度限制,以防止服务滥用

这些措施使 GraphQL 能够保持灵活性,而不会变得过于宽松。

何时 GraphQL 是(或不是)正确的选择

GraphQL 是一个非常强大的工具,但它并不适合所有用例。

它适用于以下情况:

  • 多个前端客户端使用同一个 API。
  • 数据可视化需求频繁变化。
  • 领域模型丰富且相互关联。

它不适用于以下情况:

  • 具有大量写入和高吞吐量的 API。
  • 简单的 CRUD 服务。
  • 流式或二进制数据。

选择 GraphQL 是一个架构决策,而不是一个自动的决策。

最佳实践回顾

在结束之前,总结一些已解释的概念很重要:

  • 最好优先开发模式。
  • 将模式视为公共契约。
  • 保持解析器简单且专门化。
  • 从一开始就使用 DataLoader。
  • 将 API 模型与持久化模型分离。
  • 测量并限制查询复杂度。

应用这些简单的实践比特定的框架更重要,并且往往决定了应用程序本身的成败。

结语

Spring for GraphQL 和 Netflix DGS 为 Java 开发人员提供了一个成熟的、可用于生产环境的堆栈,用于创建灵活的 API。当与 MongoDB 结合使用时,它们启用了富有表现力的数据访问模式,但前提是必须将性能、安全和模式设计视为优先事项。

GraphQL 并不旨在在所有地方取代 REST 范式。相反,它是一种有针对性的选择,在某些情况下,您希望使用一种支持更简单、更即时更改的抽象。如果使用得当,它可以成为后端和前端团队之间的共同语言,而不会牺牲 Java 开发人员所期望的健壮性和清晰度。

获取源代码以获取本文中描述的原型。

文章《GraphQL for Java Developers: Building a Flexible Data Layer》首发于 foojay