Spring进阶 - JPA持久层
本文对 Java 的持久层历史做一个简单梳理,解释了为什么会有 MyBatis 和 JPA 两种技术, 并介绍 JPA 和 Spring Data JPA 基本概念和用法。
下面让我们回到什么框架都没有的 JDBC 时代,从大的时间线 快速回顾 Java持久层历史演变。
Java 的数据库访问层技术演变
在 Java 世界里,对象(Object)生活在内存中,关系(Relation)生活在数据库里。它们就像两个语言不通的物种,而持久层框架,就是那个尽力让双方听懂彼此的翻译官 。

核心的持久层技术有:
- **JDBC:**最低抽象层,所有 SQL 与资源管理由开发者手工处理。
- **MyBatis:**将 SQL 控制权交给开发者,通过映射机制减少大量模板代码。
- **JPA(以 Hibernate 为代表):**由框架自动推断 SQL,通过 ORM 屏蔽表结构细节。
这三类技术并不是彼此替代,而是在不同业务复杂度、团队偏好和性能要求下共存。
1、JDBC - 让Java能够访问任何第三方数据库
在 JDBC 诞生之前,不同的数据库厂商协议不一致 ,Java程序无法使用一套接口(同一套代码)统一操作各种第三方数据库,于是在1997年, Sun 公司提出 JDBC 规范,由 Java 提供一套接口,第三方数据库自己写驱动,实现接口对接 Java 的 JDBC 。
JDBC 的缺点非常多:
- 连接很麻烦,样板代码多。
- 硬编码 SQL ,业务代码和 SQL 语句混合。
- 从数据库查出来的数据,需要手动转成 Java 列表。
那个年代有聪明的程序员为了减少样板代码写了多种工具类,但并没有达到理想的状态,重复工作依然存在。
2、 Hibernate 自动化的持久层框架
2001 年,Gavin King 抛出了 Hibernate, 它的核心理念是:程序员应该操作对象,而不是操作数据库表。
Hibernate 让开发者仅编写 Java 代码,可以根据对象自动生成 SQL ,简化了对数据库的操作,减少了开发者的重复劳动。
缺点:
- 自动生成的 SQL 往往性能不可控(著名的 N+1 问题)
- 由于是自动生成SQL, 对于复杂的查询显得力不从心。
3、 iBATIS 崛起 后来改名 MyBatis
Hibernate 让开发者尝到了甜头,掀起一场革命,但它的缺点也是致命的, 在处理复杂 SQL 时不够灵活 ,于是另一个持久层框架 iBATIS 便诞生(大约是2002年),跟 Hibernate 设计理念不同,iBATIS 并不是要在代码中消灭所有的 SQL 语句,而是认为SQL 是数据库的灵魂,不应该被隐藏,而应该被管理。
所以它在业界被称为 半自动化ORM, 它不负责帮你生成 SQL(虽然现在也可以),它只负责把 SQL 和 Java 代码解耦。
代码语法来到了我们熟悉的样子
Mapper接口:
▼java复制代码public interface UserMapper { User selectUserById(Long id); }
XML 配置 :
▼java复制代码<select id="selectUserById" resultType="com.example.User"> SELECT id, name, age FROM users WHERE id = #{id} </select>
半自动化胜在灵活可控,SQL 语言全由开发者手动编写,并且与 Java业务分离,独立存在 XML 配置中。
防止SQL注入的原理
#{} 可以防止SQL注入。
原理是 XML 中写的:
▼java复制代码SELECT * FROM user WHERE name = #{name}
MyBatis 会生成:
▼java复制代码SELECT * FROM user WHERE name = ?
然后立刻给数据库编译,所有主流数据库(MySQL、PostgreSQL、Oracle、SQL Server)都支持带 ? 的预编译语句
之后 MyBatis 调用 JDBC 的 API ,发送参数给数据库:
▼java复制代码preparedStatement.setString(1, name);
也就是说,分为两阶段发送:
- 数据库预编译:SQL 模板先传给数据库编译
- 数据库执行阶段:JDBC 发送参数
这就是 JDBC PreparedStatement 的防注入原理。
这样处理之后,随便用户传:name = "xxx' OR '1'='1", 最终SQL语句是:
▼java复制代码SELECT * FROM user WHERE name = "xxx' OR '1'='1"
不会把 OR 解析成关键字。
4、 JPA - Java 官方的 ORM 规范
全自动框架不止 Hibernate 一个,从2002年到2006年发展了多个全自动持久层框架,包括: Hibernate、 EclipseLink、Java 官方的 EJB 框架(做的很失败),让持久层框架的使用变得混乱(但跟 Mybatis 无关,Mybatis并非 ORM 框架,自成一派)。
于是,在2006年,Java官方(Sun 公司)提出 JPA 规范。
JPA 的本质不是一个框架,而是一套接口规范
JPA的出现是为了统一 Hibernate、EclipseLink 的使用方式,并带来了语法便利。也就是说 JPA 本身是规范,底层实现可以选用 Hibernate 或 EclipseLink 实现具体能力。
5、 当下的 Java 持久层框架的两大派系
- MyBatis 发展到如今,有 MyBatis-plus , MyBatis-flex 被主流使用,这一类属于SQL映射框架
- JPA 发展到如今, Spring Data JPA 被主流使用,这一类属于 ORM 框架
对这两个技术的定位:
Spring Data JPA : 基于 JPA 封装的规范,兼容 Spring 生态,其底层默认由 Hibernate 实现 ORM 功能。
JPA/Hibernate 的核心原理是:
根据开发者定义的实体类、字段映射、关联关系以及方法命名规则,由框架自动推导并生成 SQL。
同时,JPA 还支持 JPQL、Criteria API、Native SQL 等方式,让开发者在需要时可以手动编写 SQL。
本质上,JPA/Hibernate 是一个完整的 ORM 引擎,以“对象模型”为中心,负责对象到关系数据库之间的自动转换。
MyBatis-Plus :MyBatis-Plus 通过预置的模板 SQL、Wrapper 条件构造器、CRUD 自动生成器等,一定程度上减少了 MyBatis 中手写 SQL 的工作量。 但其核心依然是 模板化 SQL + 映射,框架并不理解实体关系,也不会根据关联规则自动生成 SQL,复杂查询仍需开发者手写 SQL。
两者在功能上是非常像的,但 MyBatis 不是 ORM 框架。
在开发的体验上,能够明显感受到 JPA 以对象为中心,MyBatis 以 「SQL 构造」和「条件注入」为中心。
在全球范围,Spring Data JPA 是绝对的主流; 在国内,则是MyBatis-Plus 主流。
JPA
介绍
JPA(Java Persistence API) 是 Java 官方发布的一套 ORM 规范 ,负责
- 要怎么把 Java 对象映射成数据库表
- 怎么维护外键关系(1对多、多对多)
- 怎么查询
- 怎么管理增删改查生命周期
但只负责一套约束,底层的具体实现代码由三个主流框架提供:
- Hibernate(最主流)
- EclipseLink
- OpenJPA
在 Spring Boot 环境中,默认的 JPA 实现就是 Hibernate。
核心理念: 把面向对象世界(对象、引用、继承、聚合)的模型,映射到关系数据库世界(表、行、列、外键、连接)的技术与模式集合。
也就是说你只需要写:
▼java复制代码User user = new User(); user.setName("张三"); entityManager.persist(user);
底层会自动生成 SQL,例如:
▼sql复制代码insert into user (name) values ('张三');
映射具体包含了:
- 类 <——>表
- 标识(Identity)映射 : Java 对象的唯一标识(主键)要和数据库的主键对应 (@Id)
- 值类型映射 : Java 的数据类型到数据库的数据类型映射,如 String, int, LocalDateTime 映射为 varchat 、int 、DateTime。
- 事务边界与一致性
EntityManager 机制
介绍
Java 对象拥有复杂的生命周期(新建、托管、游离、删除),而关系型数据库只有简单的 CRUD 操作。两者之间缺乏自动的映射机制,这在电路中叫做 阻抗失配 ,即 两个电路连接时阻抗不匹配,会导致信号反射或能量损耗 。
简单来说,java对象有生命周期(创建,更新,销毁),而在 MySQL 等数据库的 「记录」没有生命周期的概念,对象是"活的"(动态)数据载体,数据库是"死的"(静态)数据载体。
Hibernate 的解决方案: 引入 EntityManager 作为 ORM 的核心调度器, 作用是充当生命周期的过渡适配,Java对象更新后,理论上Java对象和数据库记录不一致,此时 Java对象的数据处于脏状态,EntityManager会在合适的时机自动刷新到数据库,保证数据一致性。
在 Java 代码层面,它是一个接口 (javax.persistence.EntityManager)。它的职责类似“缓存”:
- 状态管理:它负责维护实体对象在内存中的四种状态(New, Managed, Detached, Removed)。
- 脏检查(Dirty Checking):它利用内部的一级缓存机制,自动比对对象的前后状态。
- 持久化上下文: 持久化上下文是一块“一级缓存” , 在同一事务内重复读取不会重复查询数据库 ,它负责决定在何时、以何种顺序将内存中的变化生成 SQL 语句并发送给数据库,所以具有 自动脏检查(dirty check) 能力。
▼java复制代码@Transactional public void test() { User u1 = em.find(User.class, 1L); // SQL 1 次 User u2 = em.find(User.class, 1L); // 不触发 SQL(一级缓存) }
EntityManager 具体作用
1、状态管理(对象生命周期管理)
四种状态:新建(new) 、托管(Managed)、游离(Detached)、删除态 (Removed)
- 新建: Java 对象刚刚是
new出来,没有 ID (主键是空),此时 EntityManager 根本不管。 - 托管: 有 ID , EntityManager 正在监控它 ,会触发藏检查, 只要你改了 Java 对象的属性(
setName),就算你不喊保存,事务提交时,EntityManager 也会自动向数据库发UPDATE语句。
以下三种场景从 新建 变成 托管
▼java复制代码// 场景 A:通过 persist 让 New 变成 Managed em.persist(user); // 场景 B:通过 find 从数据库查出来的,天生就是 Managed User user = em.find(User.class, 1L); // 【关键】修改属性,自动同步数据库!不需要写 em.update(user) user.setName("李四");
- 游离:还是有ID, 但EntityManager 不再监控它了(可能是 Session 关闭了,或者被踢出了缓存)。 此时 你改了对象的属性,EntityManager 假装看不见,数据库不会发生任何变化。
简单来说,就是拿到数据,连接关闭,此时对象就是纯粹的 数据载体。
▼java复制代码User user = em.find(User.class, 1L); // 此时是 Managed em.clear(); // 清空上下文,把所有对象踢出管理 // 或者 em.close(); // 关闭连接 // 此时 user 变成了 Detached。 // 虽然你改了名字,但数据库里还是“李四”,不会变。 user.setName("王五");
- 删除态: 有 ID ,EntityManager 标记它为“待删除”。 事务提交时,物理删除数据库记录。
▼java复制代码User user = em.find(User.class, 1L); em.remove(user); // 标记删除 // 此时 user 对象在 Java 内存里其实还在!你可以打印它。 // 但事务提交后,数据库里就没了。
那逻辑删除怎么实现?
JPA 默认没有逻辑删除,可以由 Hibernate 偷梁换柱实现。代码如下:
▼java复制代码import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; @Entity @Table(name = "t_user") // 1. 拦截删除:告诉 Hibernate,当有人调用 remove 时,别执行 DELETE,执行这句 UPDATE @SQLDelete(sql = "UPDATE t_user SET deleted = 1 WHERE id = ?") // 2. 拦截查询:告诉 Hibernate,无论查什么,都在 WHERE 后面自动拼上这句条件 @Where(clause = "deleted = 0") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 逻辑删除标识:0-未删,1-已删 @Column(name = "deleted") private Integer deleted = 0; }
2、事务控制和延迟操作
- 它可以管理事务边界,确保一系列对象操作要么全部成功,要么全部回滚。
- 支持延迟加载(Lazy Loading),比如一对多关联对象,访问时才查询数据库,提高性能。
▼java复制代码@Service public class UserServiceManual { @PersistenceContext private EntityManager entityManager; public void createUser(User user) { // 获取事务 EntityTransaction transaction = entityManager.getTransaction(); try { // 手动开启事务 transaction.begin(); // 执行数据库操作, 这个user准备被持久化到数据库 // JPA 会在事务提交时生成对应的 INSERT SQL entityManager.persist(user); // 提交事务 transaction.commit(); } catch (Exception e) { // 异常回滚 if (transaction.isActive()) { transaction.rollback(); } throw e; } } }
事务可以替换为 Spring 注解事务:
▼java复制代码@Service public class UserServiceTransactional { @Autowired private UserRepository userRepository; // Spring Data JPA Repository @Transactional public void createUser(User user) { // 直接操作对象即可 userRepository.save(user); // 如果这里抛出异常,事务会自动回滚 // 无需手动管理 commit 或 rollback } }
EntityManager 四大核心方法
- persist() 把 new 的对象变成 managed(持久化态)
▼java复制代码User user = new User(); user.setName("Tom"); em.persist(user);
- merge() : merge 用于将 detached(脱管态) 的实体重新合并回持久化上下文。
- merge 不会让传入对象变成 managed
- merge 会复制一个新的对象并返回新的 managed 实体
▼java复制代码User user = em.find(User.class, 1L); transaction.commit(); user.setName("Tom2"); // u2 变成 managed User u2 = em.merge(user);
- remove() : 标记实体为 removed, 不是立即 DELETE , 在事务提交时删除记录 .
▼java复制代码User user = em.find(User.class, 1L); // 不会删除 Java 内存中的对象,但一般在方法内创建的对象, // 线程离开方法后,对象是【不可达】状态,下一次GC会回收 em.remove(user);
remove 必须传入一个 managed 实体,否则报错。
- refresh(): 把实体从数据库的最新数据“重置”回来 (覆盖内存中的属性)
▼java复制代码User user = em.find(User.class, 1L); user.setName("Wrong"); em.refresh(user);
JPQL 对象化查询语言
JPQL(Java Persistence Query Language)是 JPA 的查询语言,用实体类和属性名写查询,而不是数据库表名和字段名。
User→ 实体类名,而不是useru.name→ 实体字段名,而不是数据库 column 名
SQL 语句和 JPQL 语句对比
| 概念 | SQL | JPQL |
|---|---|---|
| 查询对象 | 表 t_user | 实体 User |
| 字段 | u_age | age(实体属性) |
| 表别名 | u(可选) | u(必须) |
JPQL 语言规范本身要求“实体必须带别名”
- String 拼接的 JPQL , 最基础、最常用
▼java复制代码String jpql = "SELECT u FROM User u WHERE u.age > :age"; List<User> result = em.createQuery(jpql, User.class) .setParameter("age", 20) .getResultList();
:age解释:
- 这里
:age意味着 ,这个值之后由 Java 动态赋值 :age的名字是"age".setParameter("age", 20)表示把 20 绑定进去
2. 在实体类上定义查询
▼java复制代码@Entity @NamedQuery( name = "User.findByAge", query = "SELECT u FROM User u WHERE u.age > :age" ) public class User { ... }
▼java复制代码List<User> users = em.createNamedQuery("User.findByAge", User.class) .setParameter("age", 20) .getResultList();
- Criteria API(类型安全 Query)
不写字符串 JPQL,类型安全, 多用于复杂查询、动态查询 , 用得最多的是大企业项目或动态查询场景,
▼java复制代码CriteriaBuilder cb = em.getCriteriaBuilder(); // CriteriaQuery 作用和 MyBatis-Plus 的 QueryWrapper 几乎一样 // 都是查询构造器 CriteriaQuery<User> cq = cb.createQuery(User.class); Root<User> root = cq.from(User.class); cq.select(root).where(cb.gt(root.get("age"), 20)); List<User> list = em.createQuery(cq).getResultList();
等价于:
▼java复制代码SELECT u FROM User u WHERE u.age > 20
- Native SQL(完全手写 SQL)
完全手搓SQL, 不受 JPQL 语法限制 ,有SQL 注入风险
▼java复制代码// 风险操作 String sql = "SELECT * FROM user WHERE name = '" + input + "'"; em.createNativeQuery(sql).getResultList();
安全操作 setPoarameter
▼java复制代码String sql = "SELECT * FROM user WHERE age > ?"; List<User> users = em.createNativeQuery(sql, User.class) .setParameter(1, 20) .getResultList();
后续会讲到Spring Data JPA 的语法,这里一块汇总了,不怕赘述,就怕学不会。
- Spring Data JPA 的 JPQL 方式
Spring 生态绝对主流,非常常见,没有SQL注入的风险,第一种方式 JPQL 字符串有可能导致 SQL 注入风险。
▼java复制代码@Query("SELECT u FROM User u WHERE u.age > :age") List<User> findOlderThan(@Param("age") int age);
- 通过方法名自动推断查询
不用写 JPQL,Spring 根据方法名自动生成,所以可能方法名很长。
▼java复制代码List<User> findByAgeGreaterThan(int age);
JPA 注解体系
在 JPA 规范中,实体类的行为是完全由注解驱动的。注解控制实体如何映射到表、字段、主键策略、枚举、二进制数据存储等。
基本注解
核心注解列表 :
- @Entity
- @Table
- @Id
- @GeneratedValue
- @Column
- @Transient
- @Enumerated
- @Lob
**@Entity**(必须) , 告诉 JPA “这是一个实体,请帮我管理” , 类必须有无参构造方法。
▼java复制代码@Entity public class User { }
**@Table**(可选,但推荐) , 如果类名叫User,不加这个注解,默认表名也是User。数据库通常用下划线命名(t_user),所以需要它。
▼java复制代码@Entity @Table(name = "t_user") public class User { }
**@Id**(必须) , 指定谁是主键。**@GeneratedValue**(核心) , 指定主键的生成策略,常见策略:
GenerationType.IDENTITY:数据库自增(MySQL 常用)GenerationType.SEQUENCE:序列(PostgreSQL / Oracle 使用)GenerationType.AUTO:根据数据库自动选择GenerationType.UUID(在新版 Hibernate 中已支持)
▼java复制代码@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
**@Column**(最常用) , 细粒度控制列的属性。
name:列名nullable:是否允许为 NULL(默认 true)length:VARCHAR 长度(默认 255)unique:是否创建唯一约束
▼java复制代码@Column(name = "username", nullable = false, length = 50, unique = true) private String username;
**@Transient**(重要) , 指定字段不写入数据库,不参与 ORM 映射。
使用场景:
- DTO 生成的临时字段
- 缓存某些计算结果
- 不需要被数据库持久化的业务属性(如 token)
**@Enumerated**, 指定枚举在数据库中的存储方式。
两种模式:
EnumType.STRING:把枚举值转成字符串
优点:可读、安全
缺点:占用空间稍大EnumType.ORDINAL:(禁用,全是坑)存储枚举的 ordinal(序号)
缺点:绝不能乱改枚举定义顺序!否则全表数据错位
▼java复制代码@Enumerated(EnumType.STRING) private Status status;
- @Lob ,告诉 JPA 将字段映射为大对象类型(LOB)
- 字符串 → CLOB(长文本)
- 字节数组 → BLOB(二进制)
▼java复制代码@Lob private String largeText; @Lob private byte[] fileContent;
进阶注解
做项目不可能只有一张表,只要有表关联,就逃不掉下面这些。这也是 JPA 比 MyBatis 难的地方。
- 关系映射:
**@OneToMany**/**@ManyToOne**
- 关系的“拥有方(owning side)”决定外键在哪张表上。在一对多关系中,通常由“多”的一方(
Order)持有外键user_id,因此Order是 owning side。 - 在 JPA 的双向关系中,只能一个 side 为 owning side。另一个 side 使用
mappedBy指向 owning side 的属性名(不是列名)。
有两个关键属性:
FetchType.LAZY : 懒加载 查询主实体时,不查关联。只有你 第一次访问关联字段时 才去查。用来解决表关系太多的 SQL 操作导致性能极低问题, 不需要的数据不要查,访问才查。 最好所有关联默认用懒加载
FetchType. EAGER : 立即把关联实体一起查出来
数据库表创建了外键才需要这个注解,否则不用。
▼java复制代码@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 100) private String name; // mappedBy 指向 Order 类中定义的 user 字段名 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); // helper method 保持双向一致性 public void addOrder(Order o) { orders.add(o); o.setUser(this); } public void removeOrder(Order o) { orders.remove(o); o.setUser(null); } // getters/setters... }
▼java复制代码@Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) // owning side, 指定外键列 private User user; private BigDecimal amount; // getters/setters... }
- 并发控制 乐观锁 @Version
在表上维护一个 version 列(int / long / timestamp),每次更新时 JPA 在 WHERE 子句中带上 version = ?,并在更新成功后把 version 自增(或更新为新值)
如果更新时数据库的 version 与实体中持有的 version 不一致(说明其他事务已提交修改),则更新影响行数为 0,JPA 抛出乐观锁异常(OptimisticLockException,在 Spring 中常被包装为 ObjectOptimisticLockingFailureException / OptimisticLockingFailureException)。
▼java复制代码@Entity public class Order { @Id @GeneratedValue private Long id; @Version private Integer version; private BigDecimal amount; // ... }
Spring Data JPA
介绍
Spring Data JPA 不是 JPA 的实现(它不是 Hibernate 的替代品)。 它的作用是管理 JPA。它让你连 JPA 的标准代码(如 EntityManager)都不用写了,只需要写一个接口,剩下的交给它。
Spring Data JPA 的核心理念是:约定优于配置
如果直接用 Hibernate/JPA,你可能还需要写很多通用的 DAO(数据访问对象)代码,比如增删改查。
比如:
▼java复制代码// 你需要手动管理 EntityManager,还要写 SQL 或 JPQL public User findByName(String name) { return entityManager.createQuery("SELECT u FROM User u WHERE u.name = :name", User.class) .setParameter("name", name) .getSingleResult(); }
Spring Data JPA 写法:
▼java复制代码// 你只需要定义一个接口,不需要写实现类! public interface UserRepository extends JpaRepository<User, Long> { // 只要你的方法名符合规范,SQL 会自动生成 User findByName(String name); }
你看,连实现类都不用写! Spring Data JPA 会在程序启动时,利用动态代理技术,自动帮你生成这个接口的实现代码。
核心特性
Spring Data JPA 是 JPA 的封装,对外提供了几个大杀器:
-
Repository 抽象: 它提供了
JpaRepository等顶层接口,直接继承它,你就立刻拥有了标准的 CRUD(增删改查)、分页、排序功能,一行代码都不用写。 -
方法名即查询 (Query Methods): 这是最神奇的地方。你按照语义写方法名,它自动翻译成 SQL。
-
findByAge(int age)->select * from user where age = ? -
findByNameAndAddress(String name, String addr)->select * from ... where name = ? and address = ? -
极简的分页与排序: 只需要在方法参数中加入
Pageable或Sort参数,它自动帮你拼接LIMIT等分页语句。
Repository 接口
Spring Data JPA 最核心的 4 个 Repository:
- CrudRepository
- PagingAndSortingRepository
- JpaRepository
- JpaSpecificationExecutor
看看接口的集成关系图:
▼java复制代码CrudRepository<T, ID> ↑ PagingAndSortingRepository<T, ID> ↑ JpaRepository<T, ID>
而:
▼plain复制代码JpaSpecificationExecutor<T> (完全独立,不在继承链上)
每一个接口的职责接下来展开讲。
CrudRepository —— 最基础的 CRUD
提供最基本的数据操作:
save()findById()existsById()findAll()deleteById()deleteAll()
典型用法:
▼plain复制代码userRepository.save(user); userRepository.findById(1L);
适合:简单项目基础 CRUD。
PagingAndSortingRepository —— 增加分页与排序
在 CrudRepository 基础上,扩展:
findAll(Sort sort)findAll(Pageable pageable)
用法:
▼plain复制代码Page<User> page = userRepository.findAll(PageRequest.of(0, 10));
适合:需要分页、排序,但不需要复杂 SQL 的项目。
JpaRepository —— 最强的默认 Repository
这是 Spring Data JPA 项目 最常用的接口,生产环境几乎都用它。
它扩展了:
- 批处理增强:
saveAll() - 刷新:
flush() - 批量删除:
deleteInBatch() - 去重查询:
findAll(Sort)、findAll(Pageable) - 实体管理更丰富
示例:
▼plain复制代码List<User> users = userRepository.findAll(Sort.by("age"));
适合:主流企业开发,99% 项目从它开始。
JpaSpecificationExecutor —— 完全动态查询(Criteria API)
它让你可以构建类似 MyBatis-Plus QueryWrapper 的动态条件。
提供方法:
findAll(Specification spec)findAll(Specification spec, Sort sort)findAll(Specification spec, Pageable pageable)count(Specification spec)
示例(查询 age > 20 且 name = "Tom"):
▼plain复制代码userRepository.findAll((root, query, cb) -> cb.and( cb.gt(root.get("age"), 20), cb.equal(root.get("name"), "Tom") ) );
非常像 MyBatis Plus 的构造器:
▼plain复制代码wrapper.gt("age", 20).eq("name", "Tom");
适合:复杂动态查询、企业级系统。
一个 DEMO 学会使用 Repository 接口
- 实体类定义
▼java复制代码@Entity @Data // Lombok 注解,生成 Getter/Setter 等 @Table(name = "t_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Integer age; private String email; private Date createTime; }
- 核心接口 (Repository)
这是最重要的一步。我们要同时继承两个接口 , 实现“双剑合璧”。
▼java复制代码import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; // 继承 JpaRepository 获得基础 CRUD + 分页 // 继承 JpaSpecificationExecutor 获得动态查询能力 public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 这里暂时不需要写任何代码,两个父接口已经够强大了! }
- 业务层实战使用
这里我模拟一个 Service 方法,展示如何构建复杂的动态查询。
▼java复制代码@Service public class UserService { @Autowired private UserRepository userRepository; /** * 模拟前端传入的搜索条件对象 * 如果字段为 null,说明前端没输这个条件 */ public Page<User> searchUsers(String nameKeyword, Integer minAge, String exactEmail, int page, int size) { // 1. 构建动态查询条件 (Specification) // root: 代表 User 表(也就是 SQL 中的 from t_user) // query: 顶层查询对象(主要用于组合 Order By 等,平时用得少) // cb (CriteriaBuilder): 构建 SQL 谓词的工厂(也就是 =, like, >, and, or 这些工具) Specification<User> spec = (root, query, cb) -> { // 用于存放所有的 WHERE 条件 List<Predicate> predicates = new ArrayList<>(); // --- 动态拼装条件 --- // 1. 姓名模糊查询 (对应 SQL: name LIKE %keyword%) if (nameKeyword != null && !nameKeyword.isEmpty()) { // equal 是相等,like 是模糊 predicates.add(cb.like(root.get("name"), "%" + nameKeyword + "%")); } // 2. 年龄大于某值 (对应 SQL: age > minAge) if (minAge != null) { // gt = greater than predicates.add(cb.gt(root.get("age"), minAge)); } // 3. 邮箱精确匹配 (对应 SQL: email = 'xxx') if (exactEmail != null) { predicates.add(cb.equal(root.get("email"), exactEmail)); } // --- 组合所有条件 --- // 将 List 里的条件用 AND 连接起来 return cb.and(predicates.toArray(new Predicate[0])); }; // 2. 构建分页与排序 (Pageable) // page 从 0 开始,Sort.Direction.DESC 代表倒序 Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createTime")); // 3. 发起查询 (双剑合璧调用) // findAll(Specification, Pageable) 是 JpaSpecificationExecutor 提供的 return userRepository.findAll(spec, pageable); } }
Spring Data 的查询机制
语义化查询
这是 Spring Data JPA 最“黑科技”的地方。它像一个翻译官,把你的 Java 方法名“翻译”成 SQL。
翻译规则:
Spring 会剥离方法名的前缀(如 find, read, query, count),解析剩下的部分。
-
关键字映射:
-
And/Or→WHERE ... AND ... -
Is,Equals→WHERE col = ? -
Between→WHERE col BETWEEN ? AND ? -
LessThan,GreaterThan→<,> -
Like,Containing→LIKE %?% -
OrderBy[Property][Asc/Desc]→ORDER BY ...
▼java复制代码List<User> findByEmailAndStatus(String email, Status status);
SQL:
▼java复制代码SELECT * FROM user WHERE email = ? AND status = ?
模糊匹配:
▼java复制代码List<User> findByNameLike(String name); // 传参:“%Tom%” List<User> findByNameContaining(String name); // 自动拼接成 %name% List<User> findByNameStartingWith(String prefix); // prefix%
排序:
▼java复制代码List<User> findByStatusOrderByAgeDesc(Status status);
▼java复制代码SELECT u.id, u.name, u.age, u.email, u.status FROM user u WHERE u.status = ? ORDER BY u.age DESC;
取 TopN:
▼java复制代码User findTop1ByOrderByIdDesc();
▼java复制代码SELECT u.id, u.name, u.age, u.email, u.status FROM user u ORDER BY u.id DESC LIMIT 1;
手动指定查询 (@Query 注解)
当方法名太长,或者你需要用到很复杂的 SQL 特性(如 Join 优化、子查询)时,这层机制就派上用场了。优先级高于第一层。
使用 JPQL , 操作的是类和属性,不是表和字段
▼java复制代码// ?1 代表第一个参数 @Query("select u from User u where u.email = ?1 and u.age > 18") User findAdultUserByEmail(String email);
也可以使用SQL
如果你必须用数据库特有的函数(比如 MySQL 的 DATE_FORMAT 或 Oracle 的窗口函数),就必须开启 nativeQuery = true。这时写的是纯正的 SQL。
▼java复制代码// 这里的 t_user 是表名,email 是数据库字段名 @Query(value = "select * from t_user where email = ?1", nativeQuery = true) User findByEmailNative(String email);
默认 @Query 只能查。如果你要 UPDATE 或 DELETE,必须加 @Modifying 并配合事务。
▼java复制代码@Modifying @Query("update User u set u.name = ?1 where u.id = ?2") int updateName(String name, Long id);
动态查询 Specification
当查询条件是不固定的(用户可能填了名字,也可能没填),前两种方法就“死”了。
什么是动态查询?
动态 = 根据用户输入的参数数量不同,自动拼不同的 SQL 条件
比如搜索用户页面一般有这些筛选项:
- name(可填可不填)
- minAge(可填可不填)
- maxAge(可填可不填)
- status(可选可空)
- email(可空)
用户可能输入:
| name | minAge | maxAge | status | |
|---|---|---|---|---|
| Tom | null | null | null | null |
| null | 20 | 30 | ENABLED | null |
| null | null | null | null | null |
| Lily | 30 | null | DISABLED | aaa@bb.com |
每次输入不一样 → SQL 就不一样,这就是“动态查询”。
方法名的语义化查询和 JPQL 不适合动态查询,因为那两种方式都要提前写死 SQL , 不适合灵活生成。
Specification 就是用 Java 代码动态构造 WHERE 条件的工具,类似 MyBatis-Plus 的 QueryWrapper。
必要条件:你的 Repository 必须实现 JpaSpecificationExecutor
▼java复制代码public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { }
只有实现了这个接口,Repository 才能执行:
▼plain复制代码findAll(Specification spec) findAll(Specification spec, Pageable pageable) findOne(Specification spec)
动态查询案例:
搜索条件
▼java复制代码public List<User> search(UserQuery query) { Specification<User> spec = (root, q, cb) -> { List<Predicate> list = new ArrayList<>(); if (query.getName() != null) { list.add(cb.like(root.get("name"), "%" + query.getName() + "%")); } if (query.getMinAge() != null) { list.add(cb.ge(root.get("age"), query.getMinAge())); } if (query.getMaxAge() != null) { list.add(cb.le(root.get("age"), query.getMaxAge())); } if (query.getStatus() != null) { list.add(cb.equal(root.get("status"), query.getStatus())); } if (query.getEmail() != null) { list.add(cb.equal(root.get("email"), query.getEmail())); } return cb.and(list.toArray(new Predicate[0])); }; return userRepository.findAll(spec); }
如果用户输入name , 自动生成 SQL:
▼java复制代码SELECT * FROM user WHERE name LIKE '%Tom%';
用户输入:
▼java复制代码minAge = 20 maxAge = 30 status = 'ENABLED'
生成SQL:
▼java复制代码SELECT * FROM user WHERE age >= 20 AND age <= 30 AND status = 'ENABLED';
用户全都不输入:
▼java复制代码SELECT * FROM user;
这叫 “全表查询”,因为没有条件。
分页与排序:Page, Pageable, Sort
Sort 是排序,不管你是否分页,可能都要排序(查多条数据)。
▼java复制代码// 写法 1:按 age 倒序 (ORDER BY age DESC) Sort sort1 = Sort.by(Sort.Direction.DESC, "age"); // 写法 2:更流式的写法 (推荐) Sort sort2 = Sort.by("age").descending();
Sort 对象可以用到下面分页条件中。
Spring Data 提供统一的分页接口,Pageable 分页条件,Page 分页查询结果,包含 List + TotalCount
▼plain复制代码Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "age")); Page<User> page = userRepository.findAll(pageable);
🔹 对应 SQL(第一页,5 条,按 age 倒序):
▼sql复制代码SELECT * FROM user ORDER BY age DESC LIMIT 5 OFFSET 0;
以上是本文的全部内容,介绍了ORM框架的演进,MyBatis 与 Hibernate 等框架的设计理念不同延申出来两大派系,并介绍了 JPA 和 Spring Data JPA 的基本用法,实战中基本上使用 Spring Data JPA。
