今天翻到一篇不错的技术分享,看完之后自己也琢磨了一下,把思路梳理记录下来。
1. Spring Data JPA 与 Hibernate 的关系定位
很多初学者容易把 Spring Data JPA 和 Hibernate 混为一谈,实际上它们是不同层面的东西。
- Hibernate 是一个完整的 ORM(对象关系映射)框架,负责把 Java 对象映射到数据库表,并处理 SQL 的生成与执行。
- Spring Data JPA 是 Spring 生态对 JPA 规范的封装与增强,它基于 Hibernate(或其他 JPA 实现)之上,给出了一套更简洁的 Repository 编程模型,让你无需手写大量样板代码。
spring-boot-starter-data-jpa 后,默认底层就是 Hibernate。
org.springframework.boot
spring-boot-starter-data-jpa
下面是 Spring Data JPA 与 Hibernate 的关系定位图:
Spring Data JPA
JPA 规范
Hibernate 实现
数据库
2. 实体映射核心注解:@Entity、@Table、@Column、@Id 与主键生成策略
2.1 基础注解
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, length = 50)
private String username;
@Column(name = "email", unique = true)
private String email;
// 省略 getter/setter
}
@Entity:标记该类是一个 JPA 实体。@Table:指定映射的表名,不写则默认使用类名作为表名。@Column:配置字段与列的映射关系,可指定列名、长度、是否可空、是否唯一等。@Id:标记主键字段。@GeneratedValue:指定主键生成策略。
2.2 主键生成策略对比
策略说明适用场景IDENTITY数据库自增,插入时生成主键MySQL、SQL ServerSEQUENCE使用数据库序列生成主键Oracle、PostgreSQLTABLE使用数据库表模拟序列,跨数据库通用需要跨数据库移植AUTO由 JPA 实现自动选择不推荐,行为不可控
// 使用 SEQUENCE 策略(Oracle/PostgreSQL)
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
@SequenceGenerator(name = "order_seq", sequenceName = "seq_order", allocationSize = 1)
private Long id;
}
下面是主键生成策略的选择流程图:
MySQL / SQL Server
Oracle / PostgreSQL
需要跨数据库移植
不确定
选择主键生成策略
数据库类型
IDENTITY 自增
SEQUENCE 序列
TABLE 表模拟
AUTO(不推荐)
3. 关联关系映射:@OneToMany、@ManyToOne、@ManyToMany 的配置与常用陷阱
3.1 @ManyToOne 与 @OneToMany
以「用户-订单」为例,一个用户拥有多个订单:
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List orders = new ArrayList();
}
常用陷阱:
1. 双向关联必须指定 mappedBy,否则会生成中间表。
2. cascade 要谨慎使用,CascadeType.ALL 可能导致误删关联数据。
3. orphanRemoval = true 用于删除不再关联的子实体,但要留意与 cascade 的配合。
4. 默认 @ManyToOne 是 EAGER 抓取,容易引发 N+1 坑,建议显式改为 LAZY。
3.2 @ManyToMany
以「用户-角色」为例:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "t_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set roles = new HashSet();
}
常用陷阱:
1. 集合类型建议使用 Set 而非 List,避免 Hibernate 在去重时产生意外行为。
2. 必须显式指定 @JoinTable,否则生成的中间表名和列名不可控。
3. 双向 @ManyToMany 需要一方指定 mappedBy,避免重复维护中间表。
下面是关联关系映射的整体结构图:
拥有
包含
USER
Long
id
PK
String
username
String
ORDER
Long
id
PK
Long
user_id
FK
ROLE
Long
id
PK
4. Repository 接口体系:CrudRepository → JpaRepository → JpaSpecificationExecutor
Spring Data JPA 给出了分层级的 Repository 接口,能力逐级增强:
// 第一层:基础 CRUD
public interface UserRepository extends CrudRepository {
}
// 第二层:增加分页、批量操作等 JPA 专属能力
public interface UserRepository extends JpaRepository {
}
// 第三层:增加动态查询能力(Specification)
public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {
}
接口能力对比:
接口核心能力CrudRepository<T, ID>基础增删改查、findAll、countPagingAndSortingRepository<T, ID>增加分页与排序JpaRepository<T, ID>增加批量操作、flush、getOne 等JpaSpecificationExecutor<T>增加 Specification 动态查询
下面是 Repository 接口体系的继承关系图:
Repository(标记接口)
CrudRepository
PagingAndSortingRepository
JpaRepository
JpaSpecificationExecutor(组合使用)
5. 办法名推导查询与 @Query 自定义 JPQL
5.1 办法名推导查询
Spring Data JPA 会根据办法名自动生成查询,无需编写 SQL:
public interface UserRepository extends JpaRepository {
// 按用户名精确查询
User findByUsername(String username);
// 按邮箱模糊查询
List findByEmailContaining(String keyword);
// 按年龄范围查询
List findByAgeBetween(Integer min, Integer max);
// 多条件组合
List findByUsernameAndStatus(String username, Integer status);
// 排序
List findByStatusOrderByCreateTimeDesc(Integer status);
}
方法名关键字速查:
关键字示例对应 SQLAndfindByUsernameAndStatusWHERE username=? AND status=?OrfindByUsernameOrEmailWHERE username=? OR email=?BetweenfindByAgeBetweenWHERE age BETWEEN ? AND ?ContainingfindByEmailContainingWHERE email LIKE %?%OrderByfindByStatusOrderByIdDescORDER BY id DESC
5.2 @Query 自定义 JPQL
当方法名推导无法满足复杂查询时,使用 @Query 编写 JPQL:
public interface UserRepository extends JpaRepository {
@Query("SELECT u FROM User u WHERE u.username = :username AND u.status = :status")
Optional findActiveUser(@Param("username") String username, @Param("status") Integer status);
// 使用原生 SQL
@Query(value = "SELECT * FROM t_user WHERE age > :minAge", nativeQuery = true)
List findUsersOlderThan(@Param("minAge") Integer minAge);
}
留意: 使用 nativeQuery = true 时,SQL 直接面向数据库方言,会降低可移植性,建议优先使用 JPQL。
6. 分页与排序的标准化实现
Spring Data JPA 给出了标准化的分页方案:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page getUsers(int page, int size, String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending());
return userRepository.findAll(pageable);
}
}
分页返回结果:
Page page = userService.getUsers(0, 10, "createTime");
// 当前页数据
List users = page.getContent();
// 总记录数
long total = page.getTotalElements();
// 总页数
int totalPages = page.getTotalPages();
// 是否有下一页
boolean hasNext = page.hasNext();
结合条件查询的分页:
@Query("SELECT u FROM User u WHERE u.status = :status")
Page findByStatus(@Param("status") Integer status, Pageable pageable);
7. N+1 坑的识别与解决方案(EntityGraph、JOIN FETCH)
7.1 什么是 N+1 坑
当查询 1 条主记录后,Hibernate 又为每条记录的关联对象执行 1 次查询,总共执行 1 + N 次 SQL。例如查询 10 个用户及其订单,会执行 1 次用户查询 + 10 次订单查询。
7.2 解决方案一:JOIN FETCH
public interface UserRepository extends JpaRepository {
@Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders WHERE u.status = :status")
List findUsersWithOrders(@Param("status") Integer status);
}
留意: 使用 JOIN FETCH 时,如果集合是 List,需要加 DISTINCT 去重,否则会产生重复记录。
7.3 解决方案二:@EntityGraph
public interface UserRepository extends JpaRepository {
@EntityGraph(attributePaths = {"orders", "roles"})
@Query("SELECT u FROM User u WHERE u.status = :status")
List findUsersWithDetails(@Param("status") Integer status);
}
两种方案对比:
方案优点缺点JOIN FETCH语法直观,SQL 可控多个集合同时 fetch 会产生笛卡尔积@EntityGraph声明式,可复用,支持多个属性路径底层仍生成 JOIN,同样有笛卡尔积风险
7.4 实战建议
1. 优先使用 @EntityGraph,声明式更清晰,且便于在 Repository 中复用。
2. 避免同时 fetch 多个集合,可拆分为多次查询或使用 BatchSize。
3. 使用 @BatchSize 批量抓取,减少 N+1 的查询次数:
@Entity
public class User {
@OneToMany(mappedBy = "user")
@BatchSize(size = 20)
private List orders = new ArrayList();
}
下面是 N+1 问题的识别与解决方案对比图:
方案一
方案二
方案三
查询 1 条主记录
为每条记录查询关联对象
执行 1 + N 次 SQL
如何优化?
JOIN FETCH
@EntityGraph
@BatchSize
SQL 可控,直观
声明式,可复用
批量抓取,减少查询次数
8. 总结
Spring Data JPA 是 Spring Boot 项目中最常用的持久层方案,掌握以下要点即可应对绝大多数业务场景:
1. 理解分层关系:Spring Data JPA 基于 JPA 规范,底层由 Hibernate 实现。
2. 合理设计实体映射:正确使用 @Entity、@Table、@Column、@Id 及主键策略。
3. 谨慎配置关联关系:mappedBy、cascade、orphanRemoval 是三大易错点。
4. 善用 Repository 体系:从 CrudRepository 到 JpaSpecificationExecutor 按需选择。
5. 方法名推导 + @Query 双管齐下:简单查询用方法名,复杂查询用 JPQL。
6. 标准化分页排序:Pageable + Page 是官方推荐方案。
7. 警惕 N+1 问题:JOIN FETCH、@EntityGraph、@BatchSize 是三大武器。
暂时整理到这里。以上都是个人理解,可能有疏漏,欢迎指正。
评论 (0)
暂无评论