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。

@EntityGraph:解决 N+1 的另一种方式

除了 JOIN FETCH,@EntityGraph 提供了更声明式的关联加载方式,可以在不修改 JPQL 的情况下指定哪些关联需要预加载:

@Entity
@NamedEntityGraph(
    name = "User.withOrders",
    attributeNodes = @NamedAttributeNode("orders")
)
public class User {
    // ...实体定义
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}

public interface UserRepository extends JpaRepository<User, Long> {

    // 通过 EntityGraph 声明式预加载 orders,无需修改 JPQL
    @EntityGraph("User.withOrders")
    Optional<User> findById(Long id);

    // 动态 EntityGraph(无需 @NamedEntityGraph)
    @EntityGraph(attributePaths = { "orders", "orders.items" })
    List<User> findByUsernameContaining(String keyword);
}

Specification(动态查询条件)

当查询条件是动态的(用户可能按任意字段组合过滤),JpaSpecificationExecutor 提供了类型安全的动态查询构建能力,比手写 JPQL 字符串更安全:

// Repository 继承 JpaSpecificationExecutor
public interface UserRepository
        extends JpaRepository<User, Long>,
                JpaSpecificationExecutor<User> {}

// 定义查询规格(每个规格是一个查询条件)
public class UserSpec {

    // 按用户名模糊查询
    public static Specification<User> usernameLike(String keyword) {
        return (root, query, cb) ->
            keyword == null ? null
                : cb.like(cb.lower(root.get("username")), "%" + keyword.toLowerCase() + "%");
    }

    // 按注册时间范围查询
    public static Specification<User> createdBetween(LocalDateTime from, LocalDateTime to) {
        return (root, query, cb) -> {
            if (from == null && to == null) return null;
            if (from == null) return cb.lessThanOrEqualTo(root.get("createdAt"), to);
            if (to == null)   return cb.greaterThanOrEqualTo(root.get("createdAt"), from);
            return cb.between(root.get("createdAt"), from, to);
        };
    }
}

// 在 Service 中组合查询条件
public Page<UserResponseDTO> search(UserSearchRequest req, Pageable pageable) {
    Specification<User> spec =
        Specification.where(UserSpec.usernameLike(req.getKeyword()))
                      .and(UserSpec.createdBetween(req.getFrom(), req.getTo()));

    return userRepository.findAll(spec, pageable).map(this::toDTO);
}

乐观锁与版本控制

在高并发场景中,多个用户可能同时读取并修改同一条记录。乐观锁(Optimistic Locking)通过版本号字段防止"最后写入覆盖先写入"的丢失更新问题:

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private Integer stock;

    // 版本号字段:每次 UPDATE 时自动递增
    // 若更新时 version 与数据库不匹配,抛出 OptimisticLockException
    @Version
    private Integer version;
}

// 并发场景:两个事务同时读 product(stock=10, version=1)
// 事务 A:UPDATE product SET stock=9, version=2 WHERE id=1 AND version=1 → 成功
// 事务 B:UPDATE product SET stock=9, version=2 WHERE id=1 AND version=1 → 0 行受影响
//          → Hibernate 检测到 0 行受影响 → 抛出 OptimisticLockException
// 结论:stock 正确扣减为 9(而非 8),防止了双倍扣减
Warning 乐观锁适合冲突较少的场景(读多写少)。若冲突频繁,大量 OptimisticLockException 会导致业务重试次数激增。高冲突场景(如秒杀库存扣减)应使用数据库行级悲观锁(SELECT ... FOR UPDATE)或 Redis 分布式锁。
本章小结 JPA 是 ORM 规范,Hibernate 是最流行的实现,Spring Data JPA 在其上提供了更便利的 Repository 抽象。核心要点:① N+1 问题是最常见的 ORM 性能陷阱,通过 JOIN FETCH 或 @EntityGraph 解决;② @Transactional 的 readOnly=true 优化只读查询;③ 同类内部调用无法触发事务代理;④ @Version 乐观锁防止并发写入冲突;⑤ 生产环境禁用 open-in-view。下一章学习 Spring Security + JWT 认证体系。