专栏文章
专栏文章
Spring Data 系列
1. Spring Data 系列 #01:Spring Data 简介

Spring Data 系列 #01:Spring Data 简介

发布于 2026-05-26 09:43 👁 16 次阅读
#java#spring#spring-data#jpa

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 层。

核心特性:


核心模块

模块 底层存储 典型场景
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)

暂无评论,来留下第一条吧。

发表评论