Spring Data JPA
约 1554 字大约 5 分钟
布欧-Lewyon
2026-05-15
首页 › Spring Boot › 数据访问 › Spring Data JPA
Spring Data JPA 在 JPA 规范之上提供了 Repository 抽象,大部分数据访问方法只需声明接口签名即可自动实现。
起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>spring:
jpa:
hibernate:
ddl-auto: update # 自动建表/更新表结构(开发用)
show-sql: true # 打印 SQL(调试用)
properties:
hibernate:
format_sql: true # 格式化 SQLddl-auto 选项:
| 值 | 行为 |
|---|---|
none | 不做任何变更(生产推荐) |
validate | 验证实体与表结构一致 |
update | 自动更新表结构(开发用,不推荐生产) |
create | 每次启动删除重建(测试用) |
create-drop | 启动创建,关闭删除(内嵌数据库) |
实体映射
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@Column(nullable = false)
private Integer age;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
// getter / setter
}Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 根据方法名自动推导查询
List<User> findByUsername(String username);
List<User> findByAgeGreaterThanEqual(Integer age);
Page<User> findAll(Pageable pageable);
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);
}常用方法
@Autowired
private UserRepository userRepository;
// 基础 CRUD
userRepository.save(user);
userRepository.findById(1L);
userRepository.findAll();
userRepository.count();
userRepository.deleteById(1L);
// 分页
Page<User> page = userRepository.findAll(
PageRequest.of(0, 10, Sort.by("createdAt").descending()));
page.getContent(); // 本页数据
page.getTotalPages(); // 总页数
page.getTotalElements();// 总记录数方法名查询
Spring Data 根据方法名自动生成查询,支持以下关键词:
| 关键词 | 示例 |
|---|---|
And | findByUsernameAndAge |
Or | findByUsernameOrEmail |
Between | findByAgeBetween |
LessThan / GreaterThan | findByAgeGreaterThan |
Like | findByUsernameLike |
In | findByAgeIn |
OrderBy | findAllByOrderByCreatedAtDesc |
@Query 自定义查询
对于复杂查询,使用 @Query 编写 JPQL 或原生 SQL:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.age >= :minAge ORDER BY u.createdAt DESC")
List<User> findAdultUsers(@Param("minAge") int minAge);
@Query(value = "SELECT * FROM users WHERE username LIKE %:keyword%",
countQuery = "SELECT COUNT(*) FROM users WHERE username LIKE %:keyword%",
nativeQuery = true)
Page<User> searchNative(@Param("keyword") String keyword, Pageable pageable);
}进阶阅读:JpaRepository 实现层级
初学者可以先跳过,不影响 JPA 的基本使用。
Spring Data JPA 的 Repository 接口并非直接实现,而是通过一组复杂的代理和委派层级完成:
UserRepository (interface extends JpaRepository<User, Long>)
│
└── JDK 动态代理 → RepositoryFactorySupport 创建
│
└── 代理方法调用拦截器 QueryExecutorMethodInterceptor
├── 方法名解析 → PartTreeJpaQuery → JPQL 生成
├── @Query → 直接执行配置的 JPQL
└── 通用方法 → SimpleJpaRepository 实现SimpleJpaRepository 是 JpaRepository 接口的默认实现,它本身被标注为 @Repository,其类级别声明了 @Transactional(readOnly = true):
@Repository
@Transactional(readOnly = true) // 所有查询方法默认只读事务
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
@PersistenceContext
private EntityManager entityManager; // Hibernate Session 包装
@Override
@Transactional // CRUD 方法覆盖为读写事务
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
entityManager.persist(entity); // insert
return entity;
} else {
return entityManager.merge(entity); // update
}
}
@Override
public Optional<T> findById(ID id) {
// 先检查 PersistenceContext(一级缓存)→ 缓存命中则直接返回
// 未命中则执行 SELECT
T entity = entityManager.find(getDomainClass(), id);
return Optional.ofNullable(entity);
}
}PersistenceContext(一级缓存):Hibernate 的 Session(即 JPA 的 EntityManager)维护了一个持久化上下文,它是一个 Map<EntityKey, Object>。findById 和 JPQL 查询返回的托管实体都会进入这个 Map。同一个 Session(事务)内对同一 ID 的多次查询不会发出 SQL——从一级缓存直接返回。这也是为什么 JPA 的 save() 在事务内立即 flush 后,同一个事务中的 findById 不会再查数据库。
生产意识:一级缓存的副作用:如果在一个长事务中先查了一个实体(进入一级缓存),然后在同一个事务中另一个线程/代码段修改了数据库该行数据,当前事务中的实体仍然是旧值(一级缓存不会自动刷新)。这是 JPA 长事务常见的"脏读自己"陷阱。解决方案:①缩短事务范围;②在关键读取前调用
entityManager.clear()(但会清空实体);③使用entityManager.refresh(entity)强制从数据库重新加载。另外,SimpleJpaRepository类级别声明了@Transactional(readOnly = true),这意味着即使你只调用findById(),也会开启一个只读事务——在只读场景下这略有开销(创建Session和事务对象)。@Transactional的propagation默认为REQUIRED,如果调用方已有事务则加入,不会创建新事务。
N+1 查询问题
JPA 最常见的性能陷阱是 N+1 查询:查询父实体列表后,遍历每个父实体访问延迟加载的属性,导致 1 条主查询 + N 条子查询。
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY) // 延迟加载
private List<OrderItem> items;
}
// ❌ N+1:先查 100 条订单,再每条订单逐个查商品
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
log.info("商品数: {}", order.getItems().size()); // 触发 100 次额外查询
}解决方案:
// 方案 1:JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findAllWithItems();
// 方案 2:@EntityGraph
@EntityGraph(attributePaths = "items")
@Query("SELECT o FROM Order o")
List<Order> findAllWithItems();
// 方案 3:BatchSize(延迟加载时批量抓取)
@OneToMany(fetch = FetchType.LAZY)
@BatchSize(size = 20)
private List<OrderItem> items;生产意识:
FetchType.LAZY仅在当前 Session 打开时有效。如果实体在事务外(@Transactional方法返回后)访问延迟加载属性,会抛出LazyInitializationException。常见误用是在 Controller 中直接返回实体,视图层触发延迟加载 —— 这是 N+1 的高发区。解决方案:①在 Service 层使用JOIN FETCH预加载;②使用 DTO 投影,只返回前端需要的字段。
小结
- JPA 实体用
@Entity标注,字段用@Column映射列。 JpaRepository提供基础 CRUD、分页和排序。- 方法名查询适用于简单条件,复杂查询用
@Query。 ddl-auto生产建议用validate或none,由 Flyway/Liquibase 管理 Schema。- N+1 查询是 JPA 最常见的性能陷阱,用
JOIN FETCH/@EntityGraph/@BatchSize治理。 - 易错:方法名查询的方法名必须严格遵循命名约定(如
findBy后的属性名大小写敏感)。save()对于新实体执行persist,对于已有 ID 的实体执行merge,如果手动 set ID 为已存在的值会触发 update 而非 insert。LazyInitializationException在序列化实体到 JSON 时容易触发,建议使用 DTO 或 Jackson 的@JsonIgnoreProperties控制。 - 思考任务:定义 User 实体和 UserRepository,实现
findByAgeBetween和分页查询Page<User> findAll(Pageable),编写@DataJpaTest验证查询逻辑。
上一节:JdbcTemplate
下一节:声明式事务
