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。