Chapter 03

Spring Data JPA

ORM 原理、实体映射、Repository 接口、JPQL 查询、N+1 问题与事务管理全面解析。

JPA、Hibernate 与 Spring Data JPA 的关系

这三者常被混淆,实际上是不同层次的东西:

Java 应用代码 │ ▼ Spring Data JPA ← 仓库抽象层,提供 JpaRepository 接口 │ 自动生成查询方法,简化数据访问代码 ▼ JPA(Jakarta Persistence API)← 规范/接口,定义了 ORM 的标准 API │ 如 EntityManager、@Entity、JPQL ▼ Hibernate ORM ← JPA 的默认实现(Spring Boot 自动集成) │ 也是最流行的 Java ORM 框架 ▼ JDBC ← Java 数据库连接标准 │ ▼ 数据库(PostgreSQL / MySQL / H2 等)

简言之:JPA 是规范,Hibernate 是实现,Spring Data JPA 是在 JPA 之上的便利层。日常开发中我们主要使用 Spring Data JPA 的接口,Hibernate 的细节由框架处理。

核心概念名词解释

Entity(实体)
对应数据库表的 Java 类,用 @Entity 标注。每个实体实例对应表中的一行数据。JPA 通过持久化上下文(Persistence Context)跟踪实体状态的变化。
@Id 与 @GeneratedValue
@Id 标注主键字段;@GeneratedValue 指定主键生成策略:IDENTITY(数据库自增,推荐 MySQL/PostgreSQL)、SEQUENCE(使用数据库序列)、AUTO(JPA 自动选择)、TABLE(使用额外的表存储序列,不推荐)。
Repository
Spring Data JPA 的数据访问接口。继承 JpaRepository<Entity, ID> 即可自动获得 save/findById/findAll/delete 等 CRUD 方法,无需手写 SQL 或实现类。
JPQL(Java Persistence Query Language)
面向对象的查询语言,语法类似 SQL 但操作的是实体类和属性,而非表和列。如 "SELECT u FROM User u WHERE u.email = :email",JPA 会将其翻译为对应数据库的 SQL。
N+1 查询问题
查询 N 个父实体后,访问每个父实体的懒加载关联集合时各触发 1 次 SQL,共 N+1 次查询。是 ORM 最常见的性能陷阱,应通过 JOIN FETCH 或 @EntityGraph 解决。
@Transactional
声明式事务管理注解。Spring AOP 会在方法执行前开启事务,方法正常返回时提交,抛出 RuntimeException(或指定的受检异常)时回滚。默认传播行为是 REQUIRED。

实体类设计

@Entity
@Table(name = "users")     // 对应表名,不指定则用类名小写
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String username;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String passwordHash;

    // 一个用户可以有多个订单(一对多关系)
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "orders")
@Data
@Builder
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 多个订单属于一个用户(多对一关系)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(nullable = false)
    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}

Repository 接口

// 继承 JpaRepository,泛型参数:实体类型, 主键类型
public interface UserRepository extends JpaRepository<User, Long> {

    // 方法名查询推导:Spring Data JPA 自动生成 SQL
    Optional<User> findByEmail(String email);

    List<User> findByUsernameContainingIgnoreCase(String keyword);

    boolean existsByEmail(String email);

    // JPQL 查询(当方法名推导不够用时)
    @Query("SELECT u FROM User u WHERE u.createdAt >= :since ORDER BY u.createdAt DESC")
    List<User> findRecentUsers(@Param("since") LocalDateTime since);

    // 原生 SQL 查询(nativeQuery = true)
    @Query(value = "SELECT * FROM users WHERE email ILIKE %:keyword%",
           nativeQuery = true)
    List<User> searchByEmailNative(@Param("keyword") String keyword);

    // 解决 N+1:用 JOIN FETCH 预加载关联数据
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
}

N+1 查询问题详解

N+1 问题是 ORM 使用中最高频的性能陷阱,必须深刻理解其成因和解决方案。

❌ N+1 问题示例(100 个用户 → 101 次 SQL) SELECT * FROM users LIMIT 100; ← 1 次查询 SELECT * FROM orders WHERE user_id = 1; ← 第 1 个用户的订单 SELECT * FROM orders WHERE user_id = 2; ← 第 2 个用户的订单 ... SELECT * FROM orders WHERE user_id = 100; ← 第 100 个用户的订单 ──────────────────────────────────────────── 合计:101 次数据库查询! ✅ JOIN FETCH 解决(100 个用户 → 1 次 SQL) SELECT DISTINCT u.*, o.* FROM users u LEFT JOIN orders o ON o.user_id = u.id ──────────────────────────────────────────── 合计:1 次数据库查询

事务管理

Spring 的事务管理基于 AOP 代理实现。当调用 @Transactional 方法时,Spring 创建一个代理对象,在方法执行前后注入事务逻辑。

传播行为含义使用场景
REQUIRED(默认)存在事务则加入,否则新建绝大多数业务方法
REQUIRES_NEW总是新建事务,挂起当前事务需要独立提交的操作(如审计日志)
SUPPORTS有事务则加入,没有也行只读查询方法
NOT_SUPPORTED不在事务中执行不需要事务的操作
NEVER存在事务则抛异常强制非事务场景
NESTED嵌套事务(保存点)局部回滚场景
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    // 只读事务:Spring 优化只读操作(关闭脏检查,数据库可使用只读副本)
    @Transactional(readOnly = true)
    public UserResponseDTO findById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在: " + id));
        return toDTO(user);
    }

    // 写事务:默认 REQUIRED,RuntimeException 自动回滚
    @Transactional
    public UserResponseDTO create(UserCreateDTO dto) {
        if (userRepository.existsByEmail(dto.getEmail())) {
            throw new BusinessException(409, "邮箱已被注册");
        }
        User user = User.builder()
            .username(dto.getUsername())
            .email(dto.getEmail())
            .passwordHash(passwordEncoder.encode(dto.getPassword()))
            .build();
        return toDTO(userRepository.save(user));
    }
}
Danger @Transactional 注解只在通过 Spring 代理调用时生效。如果在同一个类内部调用 @Transactional 方法(this.method()),事务不会开启——因为绕过了代理。解决方案:将被调用方法抽取到另一个 Spring Bean 中,或通过 ApplicationContext.getBean() 获取代理对象调用。
Warning 注意区分懒加载与 Open Session in View 模式。Spring Boot 默认启用 spring.jpa.open-in-view=true,这会使 Hibernate Session 在整个 HTTP 请求期间保持开启,允许在 View 层触发懒加载。但这可能导致隐性 N+1 查询,建议在生产环境禁用:spring.jpa.open-in-view=false。