时效提示:本文写于 2020 年,部分 API、配置和工具版本可能已经变化,参考时请以官方最新文档为准。

Spring Data Jpa Guide

Spring Data JPA, part of the larger Spring Data family, makes it easy to easily implement JPA based repositories. This module deals with enhanced support for JPA based data access layers. It makes it easier to build Spring-powered applications that use data access technologies.

Implementing a data access layer of an application has been cumbersome for quite a while. Too much boilerplate code has to be written to execute simple queries as well as perform pagination, and auditing. Spring Data JPA aims to significantly improve the implementation of data access layers by reducing the effort to the amount that’s actually needed. As a developer you write your repository interfaces, including custom finder methods, and Spring will provide the implementation automatically.

https://spring.io/projects/spring-data-jpa

Query Methods

Spring Data Jpa 名词: Query Creation <==> Query Methods

Spring Data Jpa Query Creation

这一部分主要是 Jpa 通过对查询方法的解析->转换成sql语句进行查询,避免了编写sql的过程.

example:

interface PersonRepository extends Repository<Person, Long> {
    // select person from person whre lastname=?1
  List<Person> findByLastname(String lastname);
}

Modifying Queries

Spring Data Jpa Modifying Queries

修改数据

@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

删除数据

interface UserRepository extends Repository<User, Long> {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query("delete from User u where user.role.id = ?1")
  void deleteInBulkByRoleId(long roleId);
}

注意事项 Jpa中除了查询都需要加@Transactional 事务

Using JPA Named Queries

使用 JPA 命名查询(Named Query)

Example

public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Using @Query

Using named queries to declare queries for entities is a valid approach and works fine for a small number of queries. As the queries themselves are tied to the Java method that executes them, you can actually bind them directly by using the Spring Data JPA @Query annotation rather than annotating them to the domain class. This frees the domain class from persistence specific information and co-locates the query to the repository interface.

Example:

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

NativeQuery=true

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

Applying Query Hints

To apply JPA query hints to the queries declared in your repository interface, you can use the @QueryHints annotation. It takes an array of JPA @QueryHint annotations plus a boolean flag to potentially disable the hints applied to the additional count query triggered when applying pagination, as shown in the following example:

Example

public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

Query To Dto

Example:

@Query(value = "select new com.park.carTrace.pojo.CarTraceResult(a.plateNo, a.plateColor, a.typeName, a.parkName, max(a.time), count(a.id)) " +
            " from CarTraceRecordEntity a where a.plateNo = ?1 and a.plateColor = ?2 " +
            "and a.type = ?3 group by a.parkNo order by time desc ")
    List<CarTraceResult> queryCarTraceRecord(String plateNo, Integer plateColor, Integer type);

from cnblog

Specifications

Spring Data Jpa Using Specifications

  • 关于一些使用Specification的技巧

    如何 使用 and or 操作

       private Predicate orPredicate(CriteriaBuilder criteriaBuilder, Predicate p1, Predicate p2) {
         if (p1 == null || p2 == null) {
             return p1 == null ? p2 : p1;
         }
         return criteriaBuilder.or(p1, p2);
     }
    
  • Jpa 关联查询Specification

Spring Data Jpa Specifications Join

Query By Example

Spring Data Jpa Query By Example

简单理解 @Transactional

Spring Data Jpa Transactional

Query 查询能够返回Stream ? 是不是很爽?

Spring Data Jpa Return Types

Other

  • Using Sort

Spring Data Jpa Using Sort

  • Using SpEL Expressions

Spring Data Jpa Using SpEl Expressions

Jpa 关联查询OneToMany ManyToOne

这三节在旧版博客里也只有标题,这次补写;示例基于 Spring Boot 2.x(javax.persistence),Spring Boot 3 以后包名改为 jakarta.persistence,写法不变。

以“用户 1 : N 订单”为例。外键在订单表,多的一方(Order)是关系维护方,用 @ManyToOne + @JoinColumn;一的一方(User)用 @OneToMany(mappedBy = ...) 表示“由对方维护”:

@Entity
@Table(name = "t_user")          // user 是部分数据库的保留字,表名加前缀更稳妥
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // mappedBy 填的是 Order 里的字段名 user,不是表字段名
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    // 双向关联要同时维护两端
    public void addOrder(Order order) {
        orders.add(order);
        order.setUser(this);
    }
    // getter / setter 省略
}

@Entity
@Table(name = "t_order")         // order 是 SQL 保留字
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private BigDecimal amount;

    @ManyToOne(fetch = FetchType.LAZY)   // @ManyToOne 默认是 EAGER,建议显式改成 LAZY
    @JoinColumn(name = "user_id")        // 外键字段
    private User user;
    // getter / setter 省略
}

Jpa 关联查询注意事项

  • 外键只由维护方写入:只往 user.getOrders() 里 add、却没调用 order.setUser(user),保存后 user_id 是 null。这是双向关联最常见的问题,所以上面用 addOrder 同时维护两端。

  • 默认抓取策略:@ManyToOne、@OneToOne 默认 EAGER;@OneToMany、@ManyToMany 默认 LAZY。

  • N+1 查询:查出 N 个用户后循环访问 user.getOrders(),每次都会再发一条 SQL,一共 1 + N 条。解决办法见下一节。

  • LazyInitializationException:在事务(Session)外访问懒加载的关联会报这个错。要么在事务内把需要的数据取完,要么用 join fetch 一次查出来。Spring Boot 默认开启 spring.jpa.open-in-view(启动时会打印警告),它让懒加载在 Controller 层也能用,但会拉长数据库连接的占用时间,建议显式设为 false。

  • 双向关联的 toString、hashCode 和 JSON 序列化会互相引用导致栈溢出:Lombok 不要直接用 @Data,关联字段加 @ToString.Exclude;接口返回用 DTO,不要直接返回实体。

  • cascade = CascadeType.ALL 包含 REMOVE,删除用户会连带删除订单;orphanRemoval = true 会删除从集合里移除的订单。用之前确认业务真的需要。

  • 同一端不能同时写 mappedBy 和 @JoinColumn,Hibernate 启动时会报 AnnotationException。

优雅使用关联查询

核心思路:按场景只查需要的数据,避免 N+1。

① JPQL join fetch:一条 SQL 把关联一起查出来:

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {

    // Hibernate 5 下一对多 join fetch 会产生重复的 User,需要 distinct(Hibernate 6 已自动去重)
    @Query("select distinct u from User u left join fetch u.orders where u.id = :id")
    Optional<User> findWithOrdersById(@Param("id") Long id);
}

② @EntityGraph:不写 JPQL,用派生查询方法也能指定要一起加载的关联:

@EntityGraph(attributePaths = "orders")
List<User> findByNameStartingWith(String prefix);

③ 投影 DTO:列表页只需要几个字段时,直接查字段,不加载实体:

public interface UserOrderCount {          // 接口投影:getter 名与查询别名一致
    String getName();
    Long getOrderCount();
}

@Query("select u.name as name, count(o) as orderCount from User u left join u.orders o group by u.id, u.name")
List<UserOrderCount> countOrders();

④ 动态条件用 Specification 做关联:

public static Specification<User> hasOrderAmountGreaterThan(BigDecimal amount) {
    return (root, query, cb) -> {
        query.distinct(true);                        // 一对多 join 会让 User 重复
        Join<User, Order> orders = root.join("orders", JoinType.INNER);
        return cb.greaterThan(orders.get("amount"), amount);
    };
}

⑤ 分页时不要 fetch 集合:对集合做 join fetch 再分页,Hibernate 会把全部数据查进内存再分页,并打印 HHH000104 警告。分页场景改用批量加载,把 N+1 变成少量 IN 查询:

spring.jpa.properties.hibernate.default_batch_fetch_size=100

参考:Spring Data JPA Query Methods、Specifications

使用注意事项

主键id不能为 int类型 进行save操作时,id为0

OneToMany中的 updateable insertable注意事项

  • Jpa查询 Native 和 非 Native区别

nativeQuery = true时,是可以执行原生sql语句的。
nativeQuery = false时, 关联的是Table的表和字段

  • Jpa 返回某些字段

定义Dto 定义构造方法,查询返回即可.