Appearance
第59课:Spring Data JPA
🎯 学习目标
- 理解 JPA、Hibernate、Spring Data JPA 三者的关系。
- 掌握 Entity 映射、Repository、方法名查询、
@Query、分页排序和事务配合。 - 能识别懒加载、N+1 查询、级联误用、Entity 直接暴露等常见问题。
- 能判断什么时候适合用 JPA,什么时候 MyBatis 或原生 SQL 更合适。
- 建立“对象模型方便开发,SQL 行为必须可控”的意识。
📖 一、JPA 是什么
JPA 是 Java Persistence API,是一套 ORM 规范。Hibernate 是最常见的 JPA 实现。Spring Data JPA 在它们之上进一步简化 Repository 开发。
关系:
text
JPA:规范
Hibernate:实现
Spring Data JPA:Repository 抽象和 Spring 集成
数据库:MySQL/PostgreSQL/Oracle 等JPA 的目标是把表映射为对象:
text
users 表 -> UserEntity 类
orders 表 -> OrderEntity 类这能减少大量 JDBC 样板代码,但也会带来 SQL 不可见、懒加载、N+1 等问题。
📖 二、依赖配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>配置:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Hong_Kong
username: root
password: root
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true生产环境不要使用 ddl-auto: update 自动改表结构。表结构变更应通过 Flyway、Liquibase 或人工审核 SQL 管理。
📖 三、Entity 映射
java
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
}常用注解:
| 注解 | 作用 |
|---|---|
@Entity | 声明持久化实体 |
@Table | 指定表名 |
@Id | 主键 |
@GeneratedValue | 主键生成策略 |
@Column | 字段映射 |
@Enumerated | 枚举映射 |
@Version | 乐观锁版本 |
枚举推荐使用字符串:
java
@Enumerated(EnumType.STRING)
private UserStatus status;不要用 EnumType.ORDINAL,枚举顺序变化会污染历史数据。
📖 四、Repository
java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
boolean existsByEmail(String email);
List<UserEntity> findByStatus(UserStatus status);
Page<UserEntity> findByStatus(UserStatus status, Pageable pageable);
}JpaRepository 提供:
text
save
findById
findAll
deleteById
count
existsById
分页排序返回单个对象时推荐 Optional<T>,避免调用方忘记空值处理。
📖 五、方法名查询
Spring Data JPA 可以根据方法名生成查询:
java
List<UserEntity> findByUsernameContaining(String keyword);
List<UserEntity> findByAgeBetween(Integer min, Integer max);
List<UserEntity> findByStatusOrderByCreatedAtDesc(UserStatus status);适合简单查询。方法名过长时可读性会下降,应改用 @Query 或 Specification。
📖 六、@Query 查询
JPQL:
java
@Query("select u from UserEntity u where u.email = :email")
Optional<UserEntity> findByEmail(@Param("email") String email);原生 SQL:
java
@Query(value = "select * from users where created_at >= :start", nativeQuery = true)
List<UserEntity> findCreatedAfter(@Param("start") LocalDateTime start);更新语句:
java
@Modifying
@Query("update UserEntity u set u.email = :email where u.id = :id")
int updateEmail(@Param("id") Long id, @Param("email") String email);@Modifying 查询需要事务。
📖 七、分页和排序
java
Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "id"));
Page<UserEntity> page = userRepository.findByStatus(UserStatus.ACTIVE, pageable);注意:PageRequest.of(0, 20) 页码从 0 开始。对外 API 可以使用从 1 开始的页码,再在 Service 中转换。
java
int pageIndex = Math.max(request.getPage(), 1) - 1;
Pageable pageable = PageRequest.of(pageIndex, request.getSize());深分页问题仍然存在。JPA 的分页最终也是 SQL limit offset,大 offset 一样慢。
📖 八、关联关系
一对多:
java
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<OrderEntity> orders = new ArrayList<>();多对一:
java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private UserEntity user;默认建议:
text
集合关联使用 LAZY。
不要随意 CascadeType.ALL。
不要直接序列化带关联的 Entity。
复杂查询明确写 fetch join 或 DTO projection。📖 九、N+1 查询
java
List<UserEntity> users = userRepository.findAll();
for (UserEntity user : users) {
user.getOrders().size();
}可能产生:
text
1 条查询用户
N 条查询每个用户订单解决方式:
java
@Query("select distinct u from UserEntity u left join fetch u.orders where u.id in :ids")
List<UserEntity> findWithOrders(@Param("ids") List<Long> ids);或使用 EntityGraph:
java
@EntityGraph(attributePaths = "orders")
List<UserEntity> findByStatus(UserStatus status);⚠️ 十、常见陷阱
1. Entity 直接返回给前端
会带来懒加载异常、循环引用、敏感字段泄露和 API 与数据库耦合。Controller 应返回 Response DTO。
2. Open Session in View
Spring Boot 曾默认开启 OSIV,让视图层也能懒加载。它会掩盖事务边界问题,并可能导致接口层触发 SQL。生产项目建议明确评估并关闭:
yaml
spring:
jpa:
open-in-view: false3. CascadeType.ALL 滥用
级联删除可能误删大量数据。级联策略必须按业务所有权设计。
4. equals/hashCode 包含关联集合
可能触发懒加载或递归。Entity 的相等性要谨慎设计。
🆚 十一、Java vs C 对比
| 维度 | C 数据访问 | Spring Data JPA |
|---|---|---|
| SQL 编写 | 手写 SQL 和结构体映射 | Repository + ORM |
| 对象映射 | 手动赋值 | Entity 自动映射 |
| 事务 | 手动 begin/commit | @Transactional |
| 性能风险 | SQL 清晰但样板多 | SQL 隐藏,需监控和日志 |
JPA 提高开发效率,但不能让你不用理解 SQL。越是复杂查询,越要关注生成的 SQL。
💡 十二、最佳实践
- 简单 CRUD 可以使用 Spring Data JPA 快速开发。
- 复杂报表、强 SQL 优化场景可以使用 MyBatis 或原生 SQL。
- Entity 不直接暴露给 API,统一转换为 DTO。
- 默认关闭或谨慎使用 OSIV。
- 关联关系默认 LAZY,按查询场景显式 fetch。
- 分页 API 注意页码从 0 还是 1。
- 生产环境表结构变更不要依赖
ddl-auto: update。 - 打开 SQL 监控,定期检查慢查询和 N+1。
🔍 十三、自测问题
text
JPA、Hibernate、Spring Data JPA 分别是什么?
为什么 Entity 不应该直接返回给前端?
方法名查询适合什么场景?
@Modifying 为什么通常需要事务?
LAZY 加载可能导致什么问题?
N+1 查询如何产生?
为什么 CascadeType.ALL 要谨慎?
JPA 什么时候不如 MyBatis 合适?🧭 十四、项目落地清单
使用 JPA 前,至少确认:
text
Entity 是否只用于持久化层?
API 是否使用 Request/Response DTO?
是否关闭或明确评估 OSIV?
是否能看到真实 SQL?
分页查询是否有索引支持?
关联查询是否评估 N+1?
级联删除是否经过业务确认?
复杂报表是否避免强行用 JPA 拼装?JPA 项目最好在测试环境打开 SQL 日志和慢查询监控:
yaml
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE生产环境不建议长期开启完整 SQL DEBUG,但必须有数据库侧慢查询监控。
🧪 十五、实战案例:DTO Projection
如果只需要返回用户列表,不一定要加载完整 Entity:
java
public record UserListItem(Long id, String username, String email) {
}
@Query("""
select new com.example.UserListItem(u.id, u.username, u.email)
from UserEntity u
where u.status = :status
order by u.id desc
""")
Page<UserListItem> findListItems(@Param("status") UserStatus status, Pageable pageable);优点:
text
只查询需要字段
避免懒加载关联
减少 Entity 状态管理成本
接口模型更清晰📌 十六、学习建议
建议分别写三种查询并观察 SQL:
text
方法名查询
JPQL @Query
DTO Projection再故意制造一次 N+1 查询,观察 SQL 数量。这个实验比单纯看 JPA 注解更有价值。
📚 十七、JPA 使用边界
JPA 适合:
text
标准 CRUD
对象关系清晰的后台管理
查询条件不太复杂的业务
团队熟悉 ORM 行为JPA 不适合:
text
复杂报表
大量手写 SQL 优化
强依赖数据库特性的查询
多表复杂聚合
对 SQL 可控性要求极高的核心链路实际项目可以混用:普通 CRUD 用 JPA,复杂查询用 MyBatis 或原生 SQL。
🎓 小结
Spring Data JPA 能极大提升 CRUD 开发效率,但它不是 SQL 的替代品。你必须理解 Entity、事务、懒加载、关联关系和生成 SQL,才能在效率和性能之间取得平衡。