Spring进阶 - JPA持久层

本文对 Java 的持久层历史做一个简单梳理,解释了为什么会有 MyBatis 和 JPA 两种技术, 并介绍 JPA 和 Spring Data JPA 基本概念和用法。

下面让我们回到什么框架都没有的 JDBC 时代,从大的时间线 快速回顾 Java持久层历史演变。

Java 的数据库访问层技术演变

在 Java 世界里,对象(Object)生活在内存中,关系(Relation)生活在数据库里。它们就像两个语言不通的物种,而持久层框架,就是那个尽力让双方听懂彼此的翻译官 。

核心的持久层技术有:

  1. **JDBC:**最低抽象层,所有 SQL 与资源管理由开发者手工处理。
  2. **MyBatis:**将 SQL 控制权交给开发者,通过映射机制减少大量模板代码。
  3. **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)。它的职责类似“缓存”:

  1. 状态管理:它负责维护实体对象在内存中的四种状态(New, Managed, Detached, Removed)。
  2. 脏检查(Dirty Checking):它利用内部的一级缓存机制,自动比对对象的前后状态。
  3. 持久化上下文: 持久化上下文是一块“一级缓存” , 在同一事务内重复读取不会重复查询数据库 ,它负责决定在何时、以何种顺序将内存中的变化生成 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)

  1. 新建: Java 对象刚刚是 new 出来,没有 ID (主键是空),此时 EntityManager 根本不管。
  2. 托管: 有 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("李四");
  1. 游离:还是有ID, 但EntityManager 不再监控它了(可能是 Session 关闭了,或者被踢出了缓存)。 此时 你改了对象的属性,EntityManager 假装看不见,数据库不会发生任何变化。

简单来说,就是拿到数据,连接关闭,此时对象就是纯粹的 数据载体。

java
复制代码
User user = em.find(User.class, 1L); // 此时是 Managed em.clear(); // 清空上下文,把所有对象踢出管理 // 或者 em.close(); // 关闭连接 // 此时 user 变成了 Detached。 // 虽然你改了名字,但数据库里还是“李四”,不会变。 user.setName("王五");
  1. 删除态: 有 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 四大核心方法

  1. persist() 把 new 的对象变成 managed(持久化态)
java
复制代码
User user = new User(); user.setName("Tom"); em.persist(user);
  1. 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);
  1. remove() : 标记实体为 removed, 不是立即 DELETE , 在事务提交时删除记录 .
java
复制代码
User user = em.find(User.class, 1L); // 不会删除 Java 内存中的对象,但一般在方法内创建的对象, // 线程离开方法后,对象是【不可达】状态,下一次GC会回收 em.remove(user);

remove 必须传入一个 managed 实体,否则报错。

  1. refresh(): 把实体从数据库的最新数据“重置”回来 (覆盖内存中的属性)
java
复制代码
User user = em.find(User.class, 1L); user.setName("Wrong"); em.refresh(user);

JPQL 对象化查询语言

JPQL(Java Persistence Query Language)是 JPA 的查询语言,用实体类和属性名写查询,而不是数据库表名和字段名。

  • User → 实体类名,而不是 user
  • u.name → 实体字段名,而不是数据库 column 名

SQL 语句和 JPQL 语句对比

概念SQLJPQL
查询对象表 t_user实体 User
字段u_ageage(实体属性)
表别名u(可选)u(必须)

JPQL 语言规范本身要求“实体必须带别名”

  1. 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();
  1. 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
  1. 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 的语法,这里一块汇总了,不怕赘述,就怕学不会。

  1. 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);
  1. 通过方法名自动推断查询

不用写 JPQL,Spring 根据方法名自动生成,所以可能方法名很长。

java
复制代码
List<User> findByAgeGreaterThan(int age);

JPA 注解体系

在 JPA 规范中,实体类的行为是完全由注解驱动的。注解控制实体如何映射到表、字段、主键策略、枚举、二进制数据存储等。

基本注解

核心注解列表 :

  • @Entity
  • @Table
  • @Id
  • @GeneratedValue
  • @Column
  • @Transient
  • @Enumerated
  • @Lob
  1. **@Entity** (必须) , 告诉 JPA “这是一个实体,请帮我管理” , 类必须有无参构造方法。
java
复制代码
@Entity public class User { }
  1. **@Table** (可选,但推荐) , 如果类名叫 User,不加这个注解,默认表名也是 User。数据库通常用下划线命名(t_user),所以需要它。
java
复制代码
@Entity @Table(name = "t_user") public class User { }
  1. **@Id** (必须) , 指定谁是主键。
  2. **@GeneratedValue** (核心) , 指定主键的生成策略,常见策略:
  • GenerationType.IDENTITY:数据库自增(MySQL 常用)
  • GenerationType.SEQUENCE:序列(PostgreSQL / Oracle 使用)
  • GenerationType.AUTO:根据数据库自动选择
  • GenerationType.UUID(在新版 Hibernate 中已支持)
java
复制代码
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
  1. **@Column** (最常用) , 细粒度控制列的属性。
  • name:列名
  • nullable:是否允许为 NULL(默认 true)
  • length:VARCHAR 长度(默认 255)
  • unique:是否创建唯一约束
java
复制代码
@Column(name = "username", nullable = false, length = 50, unique = true) private String username;
  1. **@Transient** (重要) , 指定字段不写入数据库,不参与 ORM 映射。

使用场景:

  • DTO 生成的临时字段
  • 缓存某些计算结果
  • 不需要被数据库持久化的业务属性(如 token)
  1. **@Enumerated** , 指定枚举在数据库中的存储方式。

两种模式:

  • EnumType.STRING:把枚举值转成字符串
    优点:可读、安全
    缺点:占用空间稍大
  • EnumType.ORDINAL:(禁用,全是坑)存储枚举的 ordinal(序号)
    缺点:绝不能乱改枚举定义顺序!否则全表数据错位
java
复制代码
@Enumerated(EnumType.STRING) private Status status;
  1. @Lob ,告诉 JPA 将字段映射为大对象类型(LOB)
  • 字符串 → CLOB(长文本)
  • 字节数组 → BLOB(二进制)
java
复制代码
@Lob private String largeText; @Lob private byte[] fileContent;

进阶注解

做项目不可能只有一张表,只要有表关联,就逃不掉下面这些。这也是 JPA 比 MyBatis 难的地方。

  1. 关系映射:**@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... }
  1. 并发控制 乐观锁 @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 = ?

  • 极简的分页与排序: 只需要在方法参数中加入 PageableSort 参数,它自动帮你拼接 LIMIT 等分页语句。

Repository 接口

Spring Data JPA 最核心的 4 个 Repository:

  1. CrudRepository
  2. PagingAndSortingRepository
  3. JpaRepository
  4. 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 接口

  1. 实体类定义
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; }
  1. 核心接口 (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> { // 这里暂时不需要写任何代码,两个父接口已经够强大了! }
  1. 业务层实战使用

这里我模拟一个 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 / OrWHERE ... AND ...

  • Is, EqualsWHERE col = ?

  • BetweenWHERE col BETWEEN ? AND ?

  • LessThan, GreaterThan< , >

  • Like, ContainingLIKE %?%

  • 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(可空)

用户可能输入:

nameminAgemaxAgestatusemail
Tomnullnullnullnull
null2030ENABLEDnull
nullnullnullnullnull
Lily30nullDISABLEDaaa@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。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP