时效提示:本文写于 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);
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
Query 查询能够返回Stream ? 是不是很爽?
Other
- 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 定义构造方法,查询返回即可.