1. 简介
在基于Spring Boot 项目开发中,有一个关键组件是 Spring Data JPA,这是一个在数据处理方面表现出色的 API。它在我们的开发过程中具有不可忽视的重要性。
它不仅仅是一个工具,而是一个强大的工具,能够显著提升我们的开发流程,让我们有信心和能力高效地处理复杂的数据管理任务。
最常用的默认注解包括:
- @Repository:用于标记或注解一个扮演数据访问对象(DAO)角色的类,通常我们是不需要写该注解的。
- @Query:允许开发者使用原生查询的注解。
随着时间的推移,系统的数据量也随之增大,我们都遇到过一些常见的挑战,比如查询速度慢、管理复杂的关系、理解复杂的原生查询或优化接口效率。很多人都会遇到这些问题,解决这些问题对于提升我们的开发流程至关重要。
接下来,我们将由浅入深的详细介绍Spring Data JPA实战技巧开发。
2.实战案例
2.1 基本概念
开发中,在定义接口时,我们通常可以继承:Repository,CrudRepository,PagingAndSortingRepository以及JpaRepository接口。
Repository
Repository 是最基本的接口,通常不包含任何方法。它不提供任何功能,但作为 Spring Data JPA 中所有其他仓库接口的基础接口。
public interface UserRepository extends Repository<User, Long> {
// 没有预定义的方法
}
不建议使用该接口,因为它没有提供任务功能,仅起到了标记作用。
CrudRepository
CrudRepository 接口提供了 CRUD 操作。如果你需要基本的数据访问能力而不需要排序或分页,该接口非常适合你。
public interface UserRepository extends CrudRepository<User, Long> {
// 提供了基本的 CRUD 方法
}
常用的基本方法:
- save(S entity):保存给定的实体
- findById(ID id):通过 ID 检索实体
- existsById(ID id):返回是否存在具有给定 ID 的实体
- findAll():返回所有实体
- deleteById(ID id):删除具有给定 ID 的实体
PagingAndSortingRepository
该接口添加了分页和排序的方法。这在处理大型数据集并在页面上显示数据时非常有用。这个接口也包含了 CRUD 操作。
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
// CRUD 方法加上分页和排序
}
这个接口的额外方法包括:
- findAll(Pageable pageable):返回符合 Pageable 对象中提供的分页限制的实体页面。
- findAll(Sort sort):返回按给定选项排序的所有实体。
JpaRepository
这个接口添加了 JPA 特定的方法,并提供了一套完整的 JPA 相关方法,如批量操作、自定义查询和刷新控制。这使得它成为 JPA 应用程序中最强大和最灵活的选项。
public interface UserRepository extends JpaRepository<User, Long> {
// 完整的 CRUD 方法、分页、排序和 JPA 特定方法
}
这个接口提供了一套全面的 JPA 相关操作方法,允许你执行各种任务而无需额外的接口或自定义代码。
使用 JpaRepository,你可以通过遵循 findBy 后跟属性名的命名约定来定义自定义查询方法。Spring Data JPA 将根据方法名自动生成查询。
public interface UserRepository extends JpaRepository<User, Long> {
// 通过 ID 查找实体
Optional<User> findById(Long id) ;
// 查找所有具有给定名称的实体
List<User> findByName(String name) ;
// 查找年龄大于给定值的实体
List<User> findByAgeGreaterThan(int age) ;
}
为什么选择 JpaRepository?
- 功能全面:它结合了 CRUD、分页、排序和 JPA 特定操作在一个接口中。
- 便捷性:通过提供所有必要的方法简化了代码。
- 性能:通过批量操作和刷新控制优化了性能。
- 灵活性:允许自定义查询方法并支持 JPQL(Java 持久化查询语言)和原生 SQL 查询。
2.2 使用 Specification 和 Criteria Builder
在使用 Spring Data JPA 时,有时我们需要更复杂的查询,这些查询无法通过简单的查询方法轻松实现。这时,Specification 和 Criteria Builder 就派上了用场,它们允许你构建动态查询并处理复杂的场景。
Specification
Specification 是 Spring Data JPA 中的一个函数式接口,用于基于 JPA 条件创建动态查询。它提供了一种以编程方式构建查询的方法。当查询条件在编译时未知时,它非常有用。
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class UserSpecification {
public static Specification<MyEntity> hasName(String name) {
return (Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("name"), name);
};
}
}
使用
private final UserRepository userRepository ;
Specification<User> spec = UserSpecification.hasName("Pack");
List<User> results = userRepository.findAll(spec) ;
Criteria Builder
Criteria Builder API 是 JPA 的一部分,允许创建类型安全的查询。它提供了一种使用 Java 对象而不是硬编码字符串来动态构建查询的方式。如下示例:
@Service
public class UserService {
@PersistenceContext
private EntityManager em;
public List<User> findByName(String name) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
// 构建查询
query.select(root).where(cb.equal(root.get("name"), name));
return em.createQuery(query).getResultList();
}
}
你可以结合多个条件来构建更复杂的查询。例如,你可以使用 and 和 or 来组合条件。
public class UserSpecification {
public static Specification<User> hasName(String name) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("name"), name);
};
}
public static Specification<User> hasStatus(Integer status) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("status"), status);
};
}
public static Specification<User> hasAgeGreaterThan(int age) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.greaterThan(root.get("age"), age);
};
}
public static Specification<User> hasNameAndStatus(String name, Integer status) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
Predicate namePredicate = cb.equal(root.get("name"), name);
Predicate statusPredicate = cb.equal(root.get("status"), status);
return cb.and(namePredicate, statusPredicate);
};
}
}
// 使用
Specification<User> spec = UserSpecification.hasNameAndStatus("Pack", 1) ;
List<User> results = userRepository.findAll(spec) ;
结合多个规范可以创建更灵活和可重用的查询条件。
Specification<User> spec = Specification.where(UserSpecification.hasName("Pack"))
.and(UserSpecification.hasStatus(1))
.and(UserSpecification.hasAgeGreaterThan(25));
List<User> results = myEntityRepository.findAll(spec);
2.3 开发技巧
为了最大化 Spring Data JPA 的效用,遵循一些提示和技巧是至关重要的。这些可以帮助你优化应用程序、避免常见陷阱,并确保你的代码是可维护和高效的。
使用懒加载
默认情况下,将实体关系设置为 FetchType.LAZY,这意味着相关实体在访问之前不会从数据库加载。虽然这可以节省资源,但如果处理不当,也可能导致 N+1 选择问题。
最佳实践:对于大型或很少访问的关系,使用懒加载。对于频繁访问的关系,考虑使用迫切加载。
@Entity
public class Customer {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "customer")
private List<Order> orders ;
}
优化查询
能用一个构建良好的查询完成任务时,就不要运行多个查询。必要时,使用 JPQL、Criteria API 或原生查询来优化性能。
最佳实践:使用自定义查询或规范将相关查询合并为单个数据库访问操作。
@Query("SELECT e FROM User e JOIN FETCH e.orders WHERE e.name = :name")
List<MyEntity> findByNameWithOrders(@Param("name") String name);
利用缓存
缓存可以显著提高应用程序的性能,通过减少数据库命中次数。Spring 提供了与 Ehcache、Redis 等缓存解决方案的轻松集成。
最佳实践:缓存不经常更改的频繁访问数据。
private final UserRepository userRepository ;
@Cacheable("users")
public List<User> findAll() {
return userRepository.findAll();
}
批量处理
在保存或删除多个实体时,批量处理可以减少数据库往返次数并提高性能。
最佳实践:使用 saveAll 进行批量插入,使用 deleteInBatch 进行批量删除。
private final UserRepository userRepository ;
public void saveUsers(List<User> users) {
userRepository.saveAll(users);
}
public void deleteUsers(List<User> users) {
userRepository.deleteInBatch(users);
}
适当的使用事务
确保你的数据库操作被正确地包裹在事务中,以维护数据完整性。使用 Spring 的 @Transactional 注解来管理事务。
最佳实践:在服务层使用 @Transactional,以确保方法内的所有操作都是单个事务的一部分。
@Service
public class UserService {
private final UserRepository userRepository ;
@Transactional
public void updateUsers(List<User> users) {
for (User user: users) {
userRepository.save(user) ;
}
}
}
save方法的内部是使用了 @Transactional 注解。
避免N+1问题
N+1 选择问题发生在应用程序为了获取 N 个实体的集合(每个实体都有自己的相关实体)而发出 N+1 个数据库查询时,这会严重影响性能。
最佳实践:在你的 JPQL 查询中使用 JOIN FETCH 来在单个查询中获取相关实体。
@Query("SELECT e FROM Customer e JOIN FETCH e.orders WHERE e.status = :status")
List<Customer> findByStatusWithOrders(@Param("status") Integer status);
日志记录&监控
在开发过程中启用 SQL 日志记录,以了解 Hibernate 生成的查询。这可以帮助识别和优化低效的查询。
最佳实践:使用日志记录来监控 SQL 查询和性能指标。
spring:
jpa:
show-sql=true
properties
hibernate:
'[format_sql]': true
# 慢SQL阈值
'[log_slow_query]': 1000
使用投影
有时,你只需要几个字段而不是整个实体。使用投影来仅选择必要的数据。
最佳实践:使用投影来仅获取所需的字段,减少从数据库传输的数据量。
public interface UserProjection {
String getName();
String getStatus();
}
@Query("SELECT e.name AS name, e.status AS status FROM User e WHERE e.age > :age")
List<UserProjection> queryUser(@Param("age") int age) ;
使用视图
有时,你的选择查询会变得更加复杂。创建虚拟表或表视图可以帮助简化数据访问。
最佳实践:使用视图可以简化 SELECT 语句,减少复杂性并避免潜在错误。
审计功能
Spring Data 支持实体变更审计(创建者/修改者/时间),我们能通过注解非常方便的应用审计功能。通过 @CreatedBy 和 @LastModifiedBy 来获取创建或修改实体的用户,以及 @CreatedDate 和 @LastModifiedDate 来获取更改发生的时间。
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
接下来,还需要提供如下组件用来获取用户。
@Component
public class PackAuditorAware implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.of("pack") ;
}
}
锁机制
要指定查询方法使用的锁模式,可以在查询方法上使用 @Lock 注解,具体示例如下:
public interface ProductRepository extends JpaRepository<Product, Long> {
@Transactional
@Lock(LockModeType.PESSIMISTIC_READ)
public Product findByName(String name) ;
}
注意:这里我们需要 @Transactional 事务注解。
运行输出SQL如下:
SELECT
p1_0.id,
p1_0.NAME,
p1_0.price,
p1_0.quantity
FROM
t_product p1_0
WHERE
p1_0.NAME =? FOR SHARE
自动添加了 FOR SHARE。
流式查询
可以使用 Java 8 的 Stream 作为返回类型来增量处理查询方法的结果。并非将查询结果包装在 Stream 中,而是使用特定于数据存储的方法来执行流处理,如以下示例所示:
@Query("select u from User u")
Stream<User> findUserByStream();
1.2.
流可能会封装底层特定于数据存储的资源,因此在使用后必须关闭。您可以使用 close() 方法手动关闭流,或者使用 Java 7 中的 try-with-resources 块,如下例所示:
try (Stream<User> stream = repository.findUserByStream()) {
stream.forEach(…) ;
}