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 + 平台线程 | 850 | 450ms | 50 |
| Boot 4 + 虚拟线程 | 3200 | 120ms | 20 |
优势:
- ✅ 更高的吞吐量(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 3 | Spring Boot 4 |
|---|---|---|
| Limit 支持 | ❌ 需要 Pageable | ✅ 原生 Limit |
| 滚动查询 | ❌ 无 | ✅ Window/ScrollPosition |
| Record 投影 | ⚠️ 部分支持 | ✅ 完全支持 |
| 虚拟线程 | ❌ 无 | ✅ 原生支持 |
| Hibernate 版本 | 6.1.x | 6.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
- 更好的性能
- 改进的批处理
下一步
下一章我们将学习 事务管理改进。
导航: