Spring Data Jpa 实战技巧让性能飙升

138 阅读9分钟

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(…) ;
}