在 Spring Boot 中使用 Projections(投影)按需取数据-Spring专区论坛-技术-SpringForAll社区

在 Spring Boot 中使用 Projections(投影)按需取数据

d2b5ca33bd20250104143631

Spring Boot 结合 Spring Data JPA,简化了数据驱动应用程序的开发。其中一个强大的功能是投影(Projections),它允许开发者从数据库实体中检索特定字段,而不是加载整个实体。本文将探讨投影的概念、不同类型的投影,并提供高级示例,包括使用原生查询、基于类的投影和动态投影。

什么是 Spring Boot 中的投影?

投影是 Spring Boot 中一种从实体中仅检索部分字段的方式。这在优化性能、减少内存使用和通过仅获取必要字段来保护数据访问方面特别有用。投影允许你指定所需的字段,而不是加载包含所有属性的整个实体。

Spring Boot 中的投影类型

Spring Data JPA 提供了三种主要的投影类型:

  1. 基于接口的投影(Interface-based Projections)
  2. 基于类的投影(Class-based Projections,DTO 投影)
  3. 动态投影(Dynamic Projections)

此外,投影还可以分为:

  • 封闭投影(Closed Projections)
  • 开放投影(Open Projections)

封闭投影(Closed Projections)

封闭投影限制检索的数据仅为投影接口中明确定义的字段。投影接口方法的名称必须与根实体的字段名称匹配。这确保了仅获取必要的字段,从而提供更好的性能和安全性。

示例:

假设有以下实体:User 和 Order,其中 User 可以有多个 Order

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;

    @OneToMany(mappedBy = "user")
    private List<Order> orders;
    // Getters and setters
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String product;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
    // Getters and setters
}

定义一个封闭投影,仅获取用户的 firstNameemail 和订单数量:

public interface UserSummaryProjection {
    String getFirstName();
    String getEmail();
    int getOrderCount();
}

使用此投影的 Repository 方法:

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u.firstName as firstName, u.email as email, COUNT(o) as orderCount " +
           "FROM User u LEFT JOIN u.orders o GROUP BY u")
    List<UserSummaryProjection> findUserSummaries();
}

此查询仅从 User 实体中获取必要的字段,并计算关联的 Order 数量。

开放投影(Open Projections)

开放投影允许你通过使用 SpEL(Spring Expression Language)包含自定义逻辑或派生字段。这种灵活性可能会以性能为代价,因为它可能会将整个实体加载到内存中。

示例:

扩展 UserSummaryProjection,以包含用户的全名:

public interface UserFullNameProjection {
    String getFirstName();
    String getLastName();

    @Value("#{target.firstName + ' ' + target.lastName}")
    String getFullName();
}

使用开放投影的 Repository 方法:

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserFullNameProjection> findAllBy();
}

此方法将获取 firstName 和 lastName,并通过 getFullName() 方法将这两个字段连接起来以提供全名。

基于类的投影(Class-based Projections,DTO 投影)

基于类的投影涉及将查询结果映射到数据传输对象(DTO)。对于 DTO 投影,构造函数或 getter 方法中的字段名称必须与根实体的字段名称匹配,或者显式映射。

嵌套 DTO 示例:

为 User 和 Order 创建 DTO 类:

public class OrderDTO {
    private String product;
    private double price;

    public OrderDTO(String product, double price) {
        this.product = product;
        this.price = price;
    }
    // Getters and setters
}

public class UserOrderDTO {
    private String firstName;
    private String email;
    private List<OrderDTO> orders;

    public UserOrderDTO(String firstName, String email, List<OrderDTO> orders) {
        this.firstName = firstName;
        this.email = email;
        this.orders = orders;
    }
    // Getters and setters
}

使用 JPQL 的 Repository 方法:

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT new com.example.demo.UserOrderDTO(u.firstName, u.email, " +
           "new com.example.demo.OrderDTO(o.product, o.price)) " +
           "FROM User u LEFT JOIN u.orders o")
    List<UserOrderDTO> findUserOrderDTOs();
}

此查询选择用户及其订单的摘要,并将结果映射到嵌套的 DTO 结构。

使用原生查询:

对于复杂场景,可以使用原生 SQL 查询:

public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "SELECT u.first_name as firstName, u.email as email, " +
                   "o.product as product, o.price as price " +
                   "FROM user u " +
                   "LEFT JOIN orders o ON u.id = o.user_id", nativeQuery = true)
    List<UserOrderDTO> findUserOrderDTOsNative();
}

此原生查询检索数据并将其映射到 UserOrderDTO,展示了使用原生 SQL 进行更复杂控制的场景。

动态投影(Dynamic Projections)

动态投影允许在运行时指定投影类型,从而根据上下文灵活地检索不同的数据视图。字段名称仍必须与根实体对齐或正确映射。

条件投影的高级示例:

定义多个投影接口:

public interface UserFirstNameProjection {
    String getFirstName();
}

public interface UserEmailProjection {
    String getEmail();
}

public interface UserFullDetailsProjection {
    String getFirstName();
    String getLastName();
    String getEmail();
    List<OrderDTO> getOrders();
}

支持动态投影的 Repository 方法:

public interface UserRepository extends JpaRepository<User, Long> {
    <T> List<T> findByLastName(String lastName, Class<T> type);
}

你可以在运行时动态选择使用哪种投影:

List<UserFirstNameProjection> firstNameProjections = userRepository.findByLastName("Doe", UserFirstNameProjection.class);
List<UserEmailProjection> emailProjections = userRepository.findByLastName("Doe", UserEmailProjection.class);
List<UserFullDetailsProjection> fullDetailsProjections = userRepository.findByLastName("Doe", UserFullDetailsProjection.class);

这种灵活性允许你根据不同的用例调整数据检索,而无需更改 Repository 方法签名。

何时使用每种 Projections(投影)类型

  • 封闭投影:当你需要通过仅从数据库中获取特定字段来优化性能时,最适合使用。
  • 开放投影:当你需要在投影中包含派生字段或自定义逻辑时,最适合使用。但要注意由于加载整个实体而可能导致的性能影响。
  • 基于类的投影(DTO 投影):当你需要进行复杂的数据转换或将结果集封装为更有意义的对象时,最适合使用。这在面向服务的架构中尤其有用。
  • 动态投影:当应用程序需要根据运行时条件或用户偏好灵活切换数据视图时,最适合使用。

为什么投影从一开始就很重要

投影从一开始就是 Spring Data JPA 的核心功能,解决了以下几个关键问题:

  1. 性能优化:通过仅获取必要的数据,投影减少了数据库的负载并提高了应用程序的性能。
  2. 灵活性:投影允许定制数据检索,使开发者能够满足特定需求而无需修改底层实体结构。
  3. 安全性:通过限制检索的数据,投影有助于防止敏感字段的意外暴露。
  4. 易用性:投影减少了样板代码,使处理复杂查询变得更加容易,从而提高了整体开发效率。

结论

Spring Boot 中的 Projections(投影)是一个强大的功能,可以增强数据检索策略、优化性能并提高安全性。通过理解并有效使用封闭投影、开放投影、基于类的 DTO 投影和动态投影,开发者可以创建高效、灵活且可维护的数据访问层。

无论你是构建简单的应用程序还是复杂的企业系统,选择合适的投影类型都会对应用程序的性能和可扩展性产生重大影响。动态投影尤其提供了一种多功能的方法,使你能够实时调整数据访问逻辑以满足不同的需求和条件。

请登录后发表评论

    没有回复内容