Spring Data 是 Spring 生态中统一数据访问的抽象层,屏蔽各类存储(关系型数据库、NoSQL、搜索引擎)的底层差异,让开发者用一致的 Repository 编程模型完成 CRUD、分页、动态查询等操作。
目录
| 章节 | 说明 |
|---|---|
| 是什么 | 定位与核心价值 |
| 核心模块 | 各子项目一览 |
| Repository 抽象体系 | 接口继承链与职责 |
| 查询方法派生 | Method Name Derivation |
| @Query 注解 | JPQL 与 Native Query |
| Specification 动态查询 | 运行时拼装条件 |
| Auditing 审计 | 自动填充创建/修改时间 |
| 与 Hibernate / JPA 的关系 | 分层定位 |
| 常见坑 | N+1、懒加载、findAll 等 |
是什么
Spring Data 是 Spring 官方提供的数据访问抽象框架,核心目标是:
用统一的 Repository 接口,消除不同数据存储之间的样板代码差异。
开发者只需定义接口,Spring Data 在运行时生成实现类,免去手写 DAO 层。
核心特性:
- 统一的
Repository接口体系(CRUD、分页、排序) - 方法名自动派生查询(
findByNameAndAge) @Query自定义 JPQL 或原生 SQLSpecification支持动态条件拼装Auditing自动填充时间戳和操作人- 支持事务管理、乐观锁、投影(Projection)
核心模块
| 模块 | 底层存储 | 典型场景 |
|---|---|---|
| Spring Data JPA | JPA(Hibernate) | 关系型数据库 CRUD |
| Spring Data Redis | Redis | 缓存、计数器、消息 |
| Spring Data MongoDB | MongoDB | 文档存储 |
| Spring Data Elasticsearch | Elasticsearch | 全文搜索、日志分析 |
| Spring Data JDBC | JDBC(无 ORM) | 轻量关系型,无懒加载 |
| Spring Data R2DBC | R2DBC | 响应式关系型数据库 |
| Spring Data Cassandra | Cassandra | 时序、宽列存储 |
以上模块共享同一套 Repository 抽象,切换存储时只需更换依赖和注解,业务代码改动极小。
Repository 抽象体系
Spring Data 定义了三层 Repository 接口,从通用到专用逐层扩展:
Repository(标记接口,无方法)
└── CrudRepository<T, ID>
save / findById / findAll / deleteById …
└── PagingAndSortingRepository<T, ID>
findAll(Pageable) / findAll(Sort)
└── JpaRepository<T, ID>(Spring Data JPA 专属)
saveAll / flush / saveAndFlush
findAll(Example) / deleteAllInBatch …
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.domain.User;
// 只需声明接口,Spring Data 自动生成实现
public interface UserRepository extends JpaRepository<User, Long> {
}
继承关系说明:
| 接口 | 所在模块 | 新增能力 |
|---|---|---|
Repository |
spring-data-commons | 标记接口,仅作泛型锚点 |
CrudRepository |
spring-data-commons | 增删改查 7 个基础方法 |
PagingAndSortingRepository |
spring-data-commons | 分页 + 排序 |
JpaRepository |
spring-data-jpa | 批量操作、flush、Example 查询 |
查询方法派生
Spring Data 解析方法名,自动生成 JPQL/SQL,无需手写实现。
常用关键字
| 关键字 | 示例方法名 | 生成的条件 |
|---|---|---|
findBy |
findByName |
WHERE name = ? |
findBy...And |
findByNameAndAge |
WHERE name = ? AND age = ? |
findBy...Or |
findByNameOrEmail |
WHERE name = ? OR email = ? |
findBy...Like |
findByNameLike |
WHERE name LIKE ? |
findBy...Containing |
findByNameContaining |
WHERE name LIKE %?% |
findBy...GreaterThan |
findByAgeGreaterThan |
WHERE age > ? |
findBy...Between |
findByAgeBetween |
WHERE age BETWEEN ? AND ? |
findBy...IsNull |
findByDeletedAtIsNull |
WHERE deleted_at IS NULL |
findBy...OrderBy |
findByNameOrderByAgeDesc |
ORDER BY age DESC |
countBy |
countByStatus |
SELECT count(*) WHERE status = ? |
deleteBy |
deleteByName |
DELETE WHERE name = ? |
existsBy |
existsByEmail |
SELECT count(*) > 0 |
分页示例
// 参数传入 Pageable,返回 Page<T> 自动带 totalCount
Page<User> findByStatus(String status, Pageable pageable);
// 调用方
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> page = userRepository.findByStatus("ACTIVE", pageable);
List<User> users = page.getContent();
long total = page.getTotalElements();
@Query 注解
当方法名派生不足以表达复杂查询时,使用 @Query 手写 JPQL 或原生 SQL。
JPQL(默认)
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL:操作实体类名和字段名,不是表名和列名
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findActiveByEmail(@Param("email") String email,
@Param("status") String status);
// 使用 DTO 投影:只查部分字段,避免 SELECT *
@Query("SELECT new com.example.dto.UserSummary(u.id, u.name) FROM User u WHERE u.status = 'ACTIVE'")
List<UserSummary> findActiveSummaries();
}
Native Query(原生 SQL)
// nativeQuery = true 时写真实的 SQL,操作表名和列名
@Query(value = "SELECT * FROM t_user WHERE status = :status LIMIT :limit",
nativeQuery = true)
List<User> findByStatusNative(@Param("status") String status,
@Param("limit") int limit);
JPQL vs Native Query 选择原则:优先 JPQL(可移植、类型安全);仅在需要数据库特定语法(如
ON DUPLICATE KEY UPDATE、窗口函数)时使用 Native Query。
写操作(@Modifying)
// 更新/删除必须加 @Modifying,否则抛异常
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") String status);
Specification 动态查询
适用于查询条件在运行时才能确定的场景(如搜索表单多条件筛选)。
Repository 需继承 JpaSpecificationExecutor:
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
编写 Specification(推荐抽成工具类):
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.*;
public class UserSpecs {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null : cb.equal(root.get("name"), name);
}
public static Specification<User> olderThan(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.greaterThan(root.get("age"), age);
}
}
组合使用(and / or):
Specification<User> spec = Specification
.where(UserSpecs.hasName(searchForm.getName()))
.and(UserSpecs.olderThan(searchForm.getMinAge()));
List<User> result = userRepository.findAll(spec);
返回
null的 Specification 会被自动忽略,天然支持"条件为空则不过滤"的语义。
Auditing 审计
自动在 INSERT / UPDATE 时填充创建时间、修改时间、操作人,无需手动赋值。
第一步:开启 Auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
第二步:实体类加注解
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
第三步(可选):注入当前操作人
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 从 Spring Security 上下文获取当前用户名
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(ctx -> ctx.getAuthentication())
.map(auth -> auth.getName());
}
}
与 Hibernate / JPA 的关系
应用代码
│
▼
Spring Data JPA(Repository 抽象、方法派生、@Query 解析)
│
▼
JPA 规范(javax.persistence / jakarta.persistence API)
│
▼
Hibernate(JPA 的默认实现:生成 SQL、管理 Session、缓存)
│
▼
JDBC / 数据库驱动
│
▼
数据库(MySQL / PostgreSQL / H2 …)
| 层 | 职责 | 是否可替换 |
|---|---|---|
| Spring Data JPA | 生成 Repository 代理、解析方法名、管理事务 | 可替换为 Spring Data JDBC |
| JPA 规范 | 定义 EntityManager、@Entity、@OneToMany 等标准 API |
规范,不可替换 |
| Hibernate | JPA 的具体实现,生成 SQL,管理一级/二级缓存 | 可替换为 EclipseLink 等 |
Spring Data JPA 并不绕过 Hibernate,而是在其之上封装了 Repository 层。
EntityManager的生命周期仍由 Hibernate 管理。
常见坑
N+1 问题
现象:查询 100 条订单,每条订单再发一条 SQL 查关联用户,共 101 条 SQL。
原因:@ManyToOne / @OneToMany 默认懒加载,循环访问关联属性时触发额外查询。
解决方案:
// 方案 1:@Query 用 JOIN FETCH 一次性加载
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")
List<Order> findWithUser(@Param("status") String status);
// 方案 2:@EntityGraph 声明式指定预加载路径
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findByStatus(String status);
懒加载在事务外抛 LazyInitializationException
现象:could not initialize proxy - no Session。
原因:懒加载需要 Hibernate Session,Session 在事务结束后关闭。在 Controller 层或异步线程访问懒加载属性时,Session 已不存在。
解决方案:
// 方案 1:Service 方法加 @Transactional,在事务内完成所有属性访问
@Transactional(readOnly = true)
public OrderVO getOrderDetail(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
// 在事务内访问懒加载属性
order.getUser().getName();
return toVO(order);
}
// 方案 2:改为 EAGER 加载(谨慎,容易引入 N+1)
@ManyToOne(fetch = FetchType.EAGER)
private User user;
findAll() 全表扫描性能问题
现象:userRepository.findAll() 在大表上极慢甚至 OOM。
原因:findAll() 无条件查全表,加载所有行到内存。
解决方案:
// 始终传入 Pageable,不允许无分页的全量查询
Page<User> findAll(Pageable pageable);
// 或加条件
List<User> findByStatus(String status);
@Transactional 在同类内部调用失效
现象:同一个类中方法 A 调用加了 @Transactional 的方法 B,事务不生效。
原因:Spring 事务基于 AOP 代理,内部调用绕过代理,事务注解失效。
解决方案:将 B 方法移到另一个 Bean,或通过 AopContext.currentProxy() 获取代理对象调用。
save() 在已有 ID 时执行 UPDATE 还是 INSERT?
// Spring Data JPA 的 SimpleJpaRepository.save() 逻辑:
// entity.getId() == null → persist(INSERT)
// entity.getId() != null → merge(UPDATE,无论记录是否存在)
对于手动指定 ID 的实体(如雪花 ID),save() 会先 SELECT 再决定,产生额外查询。可实现
Persistable<ID>接口并重写isNew()来优化。
参考资料
- Spring Data JPA 官方文档:
https://docs.spring.io/spring-data/jpa/reference/(网络受限,链接未经实时验证,请直接访问)- Spring Data Commons 官方文档:
https://docs.spring.io/spring-data/commons/reference/(网络受限,链接未经实时验证,请直接访问)
评论 (0)
发表评论