Skip to content

第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&lt;T&gt;,避免调用方忘记空值处理。


📖 五、方法名查询

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: false

3. 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,才能在效率和性能之间取得平衡。