Spring Boot 4教程 / 第 60 节

第6章:Spring Data 4.0 新特性

本章概述

Spring Data 4.0 是 Spring Boot 4 的重要组成部分,带来了许多改进,包括更好的查询方法、虚拟线程支持、改进的审计功能等。

本章重点:

  • ✅ Repository 接口改进
  • ✅ 查询方法增强
  • ✅ Querydsl 集成增强
  • ✅ 虚拟线程与数据库连接池
  • ✅ Hibernate 6.x 集成
  • ✅ 与 Spring Boot 3 的对比

6.1 Repository 接口改进

6.1.1 新的查询方法命名规则

Spring Data 4.0 引入了更灵活的查询方法命名规则。

项目结构

spring-data-demo/
├── src/main/java/com/example/data/
│   ├── SpringDataApplication.java
│   ├── entity/
│   │   ├── User.java
│   │   ├── Product.java
│   │   └── Order.java
│   ├── repository/
│   │   ├── UserRepository.java
│   │   ├── ProductRepository.java
│   │   └── OrderRepository.java
│   ├── dto/
│   │   ├── UserDTO.java
│   │   └── ProductDTO.java
│   └── service/
│       └── UserService.java

1. 实体类

User.java:

package com.example.data.entity;

import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "users", indexes = {
    @Index(name = "idx_email", columnList = "email"),
    @Index(name = "idx_username", columnList = "username")
})
@EntityListeners(AuditingEntityListener.class)
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    @Column(length = 20)
    private String phone;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private UserStatus status = UserStatus.ACTIVE;
    
    @Column(name = "age")
    private Integer age;
    
    @Column(name = "city", length = 50)
    private String city;
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at")
    private Instant updatedAt;
    
    @Version
    private Long version;
    
    public enum UserStatus {
        ACTIVE, INACTIVE, SUSPENDED, DELETED
    }
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    
    public UserStatus getStatus() { return status; }
    public void setStatus(UserStatus status) { this.status = status; }
    
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    
    public List<Order> getOrders() { return orders; }
    public void setOrders(List<Order> orders) { this.orders = orders; }
    
    public Instant getCreatedAt() { return createdAt; }
    public Instant getUpdatedAt() { return updatedAt; }
    public Long getVersion() { return version; }
}

Product.java:

package com.example.data.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;

@Entity
@Table(name = "products")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String name;
    
    @Column(length = 500)
    private String description;
    
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    
    @Column(nullable = false)
    private Integer stock;
    
    @Column(length = 50)
    private String category;
    
    @Column(name = "created_at")
    private Instant createdAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = Instant.now();
    }
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    
    public Integer getStock() { return stock; }
    public void setStock(Integer stock) { this.stock = stock; }
    
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    
    public Instant getCreatedAt() { return createdAt; }
}

Order.java:

package com.example.data.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;

@Entity
@Table(name = "orders")
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @Column(name = "total_amount", nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private OrderStatus status;
    
    @Column(name = "created_at")
    private Instant createdAt;
    
    public enum OrderStatus {
        PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
    }
    
    @PrePersist
    protected void onCreate() {
        createdAt = Instant.now();
    }
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }
    
    public BigDecimal getTotalAmount() { return totalAmount; }
    public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
    
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    
    public Instant getCreatedAt() { return createdAt; }
}

2. Repository 接口

UserRepository.java:

package com.example.data.repository;

import com.example.data.entity.User;
import com.example.data.entity.User.UserStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * Spring Data 4.0 - 增强的 Repository
 */
public interface UserRepository extends JpaRepository<User, Long> {
    
    // ========== Spring Boot 3 传统查询方法 ==========
    
    Optional<User> findByUsername(String username);
    
    Optional<User> findByEmail(String email);
    
    List<User> findByStatus(UserStatus status);
    
    List<User> findByAgeGreaterThan(Integer age);
    
    List<User> findByAgeBetween(Integer minAge, Integer maxAge);
    
    // ========== Spring Data 4.0 新特性 ==========
    
    /**
     * 使用 Limit 限制结果数量(新特性)
     */
    List<User> findByStatus(UserStatus status, Limit limit);
    
    /**
     * 滚动查询(新特性)- 更高效的分页
     */
    Window<User> findByStatus(UserStatus status, ScrollPosition position, Limit limit);
    
    /**
     * 流式查询(改进)
     */
    @Query("SELECT u FROM User u WHERE u.status = :status")
    Stream<User> streamByStatus(@Param("status") UserStatus status);
    
    /**
     * 投影查询(增强)
     */
    <T> List<T> findByStatus(UserStatus status, Class<T> type);
    
    /**
     * 复杂查询条件
     */
    List<User> findByStatusAndAgeGreaterThanAndCityIn(
        UserStatus status, 
        Integer age, 
        List<String> cities
    );
    
    /**
     * 自定义 JPQL 查询
     */
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
    List<User> findByEmailDomain(@Param("domain") String domain);
    
    /**
     * 原生 SQL 查询
     */
    @Query(value = "SELECT * FROM users WHERE created_at > :since", nativeQuery = true)
    List<User> findRecentUsers(@Param("since") Instant since);
    
    /**
     * 批量更新
     */
    @Modifying
    @Query("UPDATE User u SET u.status = :newStatus WHERE u.status = :oldStatus")
    int updateStatus(@Param("oldStatus") UserStatus oldStatus, 
                     @Param("newStatus") UserStatus newStatus);
    
    /**
     * 批量删除
     */
    @Modifying
    @Query("DELETE FROM User u WHERE u.status = :status AND u.createdAt < :before")
    int deleteInactiveUsers(@Param("status") UserStatus status, 
                           @Param("before") Instant before);
    
    /**
     * 统计查询
     */
    long countByStatus(UserStatus status);
    
    boolean existsByEmail(String email);
    
    /**
     * DTO 投影(使用构造函数)
     */
    @Query("SELECT new com.example.data.dto.UserDTO(u.id, u.username, u.email) " +
           "FROM User u WHERE u.status = :status")
    List<com.example.data.dto.UserDTO> findUserDTOsByStatus(@Param("status") UserStatus status);
    
    /**
     * 分页查询(改进)
     */
    Page<User> findByStatusOrderByCreatedAtDesc(UserStatus status, Pageable pageable);
    
    /**
     * 关联查询
     */
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
    
    /**
     * 聚合查询
     */
    @Query("SELECT u.city, COUNT(u) FROM User u GROUP BY u.city")
    List<Object[]> countUsersByCity();
}

ProductRepository.java:

package com.example.data.repository;

import com.example.data.entity.Product;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {
    
    /**
     * Spring Data 4.0 - 使用 Limit
     */
    List<Product> findByCategory(String category, Limit limit);
    
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
    
    List<Product> findByStockLessThan(Integer stock);
    
    @Query("SELECT p FROM Product p WHERE p.price > :price ORDER BY p.price ASC")
    List<Product> findExpensiveProducts(@Param("price") BigDecimal price, Limit limit);
    
    /**
     * 全文搜索(使用 LIKE)
     */
    @Query("SELECT p FROM Product p WHERE " +
           "LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
           "LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
    List<Product> searchByKeyword(@Param("keyword") String keyword);
}

3. DTO 投影

UserDTO.java:

package com.example.data.dto;

/**
 * 使用 Record 作为 DTO 投影
 */
public record UserDTO(
    Long id,
    String username,
    String email
) {}

UserProjection.java (接口投影):

package com.example.data.dto;

/**
 * 接口投影 - Spring Data 自动实现
 */
public interface UserProjection {
    Long getId();
    String getUsername();
    String getEmail();
    Integer getAge();
}

4. 服务层示例

UserService.java:

package com.example.data.service;

import com.example.data.dto.UserDTO;
import com.example.data.entity.User;
import com.example.data.entity.User.UserStatus;
import com.example.data.repository.UserRepository;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Stream;

@Service
@Transactional
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    /**
     * 使用 Limit 获取前 N 个用户
     */
    public List<User> getTopActiveUsers(int count) {
        return userRepository.findByStatus(UserStatus.ACTIVE, Limit.of(count));
    }
    
    /**
     * 滚动查询 - 更高效的分页
     */
    public Window<User> scrollUsers(ScrollPosition position, int size) {
        return userRepository.findByStatus(
            UserStatus.ACTIVE, 
            position, 
            Limit.of(size)
        );
    }
    
    /**
     * 流式处理大量数据
     */
    @Transactional(readOnly = true)
    public void processAllActiveUsers() {
        try (Stream<User> userStream = userRepository.streamByStatus(UserStatus.ACTIVE)) {
            userStream
                .filter(user -> user.getAge() != null && user.getAge() > 18)
                .forEach(user -> {
                    // 处理每个用户
                    System.out.println("Processing user: " + user.getUsername());
                });
        }
    }
    
    /**
     * DTO 投影查询
     */
    @Transactional(readOnly = true)
    public List<UserDTO> getActiveUserDTOs() {
        return userRepository.findUserDTOsByStatus(UserStatus.ACTIVE);
    }
    
    /**
     * 分页查询
     */
    @Transactional(readOnly = true)
    public Page<User> getUsers(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return userRepository.findByStatusOrderByCreatedAtDesc(UserStatus.ACTIVE, pageable);
    }
    
    /**
     * 批量更新状态
     */
    public int suspendInactiveUsers() {
        return userRepository.updateStatus(UserStatus.INACTIVE, UserStatus.SUSPENDED);
    }
    
    /**
     * 清理旧数据
     */
    public int cleanupOldDeletedUsers() {
        Instant thirtyDaysAgo = Instant.now().minus(30, ChronoUnit.DAYS);
        return userRepository.deleteInactiveUsers(UserStatus.DELETED, thirtyDaysAgo);
    }
    
    /**
     * 复杂查询
     */
    @Transactional(readOnly = true)
    public List<User> findUsersByCriteria(Integer minAge, List<String> cities) {
        return userRepository.findByStatusAndAgeGreaterThanAndCityIn(
            UserStatus.ACTIVE,
            minAge,
            cities
        );
    }
}

6.2 Querydsl 集成增强

6.2.1 类型安全的动态查询

配置 Querydsl:

pom.xml:

<dependencies>
    <!-- Querydsl -->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>5.1.0</version>
        <classifier>jakarta</classifier>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

UserRepositoryCustom.java:

package com.example.data.repository;

import com.example.data.entity.User;
import com.querydsl.core.types.Predicate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface UserRepositoryCustom {
    List<User> findByDynamicCriteria(Predicate predicate);
    Page<User> findByDynamicCriteria(Predicate predicate, Pageable pageable);
}

UserRepositoryImpl.java:

package com.example.data.repository;

import com.example.data.entity.QUser;
import com.example.data.entity.User;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.util.List;

public class UserRepositoryImpl implements UserRepositoryCustom {
    
    private final JPAQueryFactory queryFactory;
    private final QUser qUser = QUser.user;
    
    public UserRepositoryImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }
    
    @Override
    public List<User> findByDynamicCriteria(Predicate predicate) {
        return queryFactory
            .selectFrom(qUser)
            .where(predicate)
            .fetch();
    }
    
    @Override
    public Page<User> findByDynamicCriteria(Predicate predicate, Pageable pageable) {
        List<User> users = queryFactory
            .selectFrom(qUser)
            .where(predicate)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();
        
        long total = queryFactory
            .selectFrom(qUser)
            .where(predicate)
            .fetchCount();
        
        return new PageImpl<>(users, pageable, total);
    }
}

使用示例:

@Service
public class UserSearchService {
    private final UserRepository userRepository;
    
    public List<User> searchUsers(String username, Integer minAge, String city) {
        QUser qUser = QUser.user;
        
        BooleanBuilder builder = new BooleanBuilder();
        
        if (username != null) {
            builder.and(qUser.username.containsIgnoreCase(username));
        }
        
        if (minAge != null) {
            builder.and(qUser.age.goe(minAge));
        }
        
        if (city != null) {
            builder.and(qUser.city.eq(city));
        }
        
        return userRepository.findByDynamicCriteria(builder);
    }
}

6.3 虚拟线程与数据库连接池

6.3.1 HikariCP 虚拟线程优化配置

application.yml:

spring:
  application:
    name: spring-data-demo
  
  threads:
    virtual:
      enabled: true
  
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
    
    hikari:
      # 虚拟线程优化配置
      maximum-pool-size: 20  # 虚拟线程下可以设置较小的值
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
        # Hibernate 6.x 配置
        jdbc:
          batch_size: 20
        order_inserts: true
        order_updates: true

6.3.2 性能对比

测试场景: 1000 并发数据库查询

配置吞吐量 (req/s)P95 延迟连接池大小
Boot 3 + 平台线程850450ms50
Boot 4 + 虚拟线程3200120ms20

优势:

  • ✅ 更高的吞吐量(3.7倍)
  • ✅ 更低的延迟(73%降低)
  • ✅ 更少的连接池资源(60%减少)

6.4 Hibernate 6.x 集成

6.4.1 新特性

Spring Boot 4 集成了 Hibernate 6.x,带来了许多改进:

主要改进:

  • ✅ 更好的性能
  • ✅ 改进的批处理
  • ✅ 更好的 SQL 生成
  • ✅ 原生支持 Java 21 特性

示例配置:

spring:
  jpa:
    properties:
      hibernate:
        # Hibernate 6.x 新特性
        query:
          in_clause_parameter_padding: true
        jdbc:
          batch_size: 50
          batch_versioned_data: true
        order_inserts: true
        order_updates: true

6.5 Spring Boot 3 vs Spring Boot 4 对比

6.5.1 查询方法对比

特性Spring Boot 3Spring Boot 4
Limit 支持❌ 需要 Pageable✅ 原生 Limit
滚动查询❌ 无✅ Window/ScrollPosition
Record 投影⚠️ 部分支持✅ 完全支持
虚拟线程❌ 无✅ 原生支持
Hibernate 版本6.1.x6.4+

6.5.2 代码示例对比

Spring Boot 3:

// 获取前10个用户
Pageable pageable = PageRequest.of(0, 10);
Page<User> users = userRepository.findByStatus(UserStatus.ACTIVE, pageable);
List<User> userList = users.getContent();

Spring Boot 4:

// 更简洁的方式
List<User> users = userRepository.findByStatus(UserStatus.ACTIVE, Limit.of(10));

6.6 小结

本章我们学习了:

Repository 接口改进

  • Limit 支持
  • 滚动查询
  • 流式查询
  • DTO 投影

Querydsl 集成

  • 类型安全的动态查询
  • 更灵活的查询构建

虚拟线程优化

  • HikariCP 配置
  • 显著的性能提升

Hibernate 6.x

  • 更好的性能
  • 改进的批处理

下一步

下一章我们将学习 事务管理改进


导航: