JPA 入门指南:从零到动态查询,一篇搞定
本文适合:刚接触 Spring Boot + JPA 的开发者,或想回顾 JPA 动态查询用法的同学。
阅读时间:约 15 分钟
一、JPA 是什么?一句话解释
JPA(Java Persistence API) 就是让你用 Java 对象来操作数据库,不用写 SQL。
听起来很抽象?看张图就懂了:
┌─────────────────────────────────────────────────────┐
│ Java 代码 │
│ │
│ User user = new User(); │
│ user.setName("张三"); │
│ user.setAge(25); │
│ userRepository.save(user); ← 一行代码,搞定插入 │
│ │
└──────────────────────┬──────────────────────────────┘
│ JPA 自动转换
▼
┌─────────────────────────────────────────────────────┐
│ 数据库 │
│ │
│ INSERT INTO user (name, age) VALUES ('张三', 25); │
│ │
└─────────────────────────────────────────────────────┘
核心思想:你操作对象,JPA 帮你翻译成 SQL。 这就是 ORM(Object-Relational Mapping,对象关系映射)。
二、5 分钟搭好环境
2.1 引入依赖(pom.xml)
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库驱动(以 MySQL 为例) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
2.2 配置数据库(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update # 自动建表(开发用,生产环境慎用)
show-sql: true # 控制台打印 SQL,方便调试
环境就绪,开始写代码!
三、第一个实体类:把数据库表变成 Java 对象
@Entity // 告诉 JPA:这是一个实体类,对应一张表
@Table(name = "t_user") // 指定表名(不写默认用类名)
public class User {
@Id // 主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增
private Long id;
@Column(nullable = false, length = 50) // 字段约束
private String name;
private Integer age;
@Column(length = 100)
private String email;
@Enumerated(EnumType.STRING) // 枚举类型存字符串
private UserStatus status;
@CreationTimestamp // 自动填充创建时间
private LocalDateTime createTime;
// getter / setter / 构造函数(Lombok 的 @Data 可以省略)
}
对应关系一目了然:
Java 实体类 User 数据库表 t_user
┌──────────────┐ ┌──────────────┐
│ id (Long) │ ──────── │ id (BIGINT) │
│ name (String) │ ──────── │ name (VARCHAR)│
│ age (Integer) │ ──────── │ age (INT) │
│ email (String)│ ──────── │ email (VARCHAR)│
│ status (Enum) │ ──────── │ status (VARCHAR)│
│ createTime │ ──────── │ create_time │
└──────────────┘ └──────────────┘
小贴士:用了 Lombok 的
@Data注解,getter/setter 全都不用写,代码瞬间清爽。
四、Repository:数据访问层,JPA 的灵魂
4.1 基本用法
public interface UserRepository extends JpaRepository<User, Long> {
// 继承 JpaRepository,什么都不写,就已经拥有 17 个方法!
}
继承后自动获得的方法:
| 方法 | 作用 | 示例 |
|---|---|---|
save() |
保存/更新 | userRepository.save(user) |
findById() |
按 ID 查询 | userRepository.findById(1L) |
findAll() |
查询全部 | userRepository.findAll() |
deleteById() |
按 ID 删除 | userRepository.deleteById(1L) |
count() |
计数 | userRepository.count() |
existsById() |
判断是否存在 | userRepository.existsById(1L) |
一行 SQL 都不用写,CRUD 就完成了!
4.2 方法名查询:JPA 的魔法
JPA 可以根据方法名自动生成 SQL,规则很简单:
public interface UserRepository extends JpaRepository<User, Long> {
// 方法名拆解:findBy + Name → WHERE name = ?
List<User> findByName(String name);
// findBy + Name + And + Age → WHERE name = ? AND age = ?
List<User> findByNameAndAge(String name, Integer age);
// findBy + Name + Or + Email → WHERE name = ? OR email = ?
List<User> findByNameOrEmail(String name, String email);
// findBy + Age + GreaterThan → WHERE age > ?
List<User> findByAgeGreaterThan(Integer age);
// findBy + Name + Like → WHERE name LIKE ?
List<User> findByNameLike(String name);
// findBy + Name + OrderBy + Age + Desc → WHERE name = ? ORDER BY age DESC
List<User> findByNameOrderByAgeDesc(String name);
// findBy + Status + Not → WHERE status != ?
List<User> findByStatusNot(UserStatus status);
// findBy + CreateTime + Between → WHERE create_time BETWEEN ? AND ?
List<User> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
}
命名规则速查:
findByName → = (等于)
findByNameLike → LIKE
findByNameContaining→ LIKE '%关键词%'(最常用!)
findByAgeGreaterThan→ >
findByAgeLessThan → <
findByAgeBetween → BETWEEN
findByStatusIn → IN
findByCreateTimeAfter → >
findByNameIsNull → IS NULL
findByNameOrderByAgeDesc → ORDER BY
经验之谈:
Containing比Like更常用,因为自动加%,不用手动拼接。
4.3 JPQL 查询:当方法名不够用时
方法名太长?用 @Query 写自定义查询:
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL:操作的是实体类,不是数据库表!
@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);
// 原生 SQL:用 nativeQuery = true
@Query(value = "SELECT * FROM t_user WHERE age > :age", nativeQuery = true)
List<User> findByAgeNative(@Param("age") Integer age);
// 更新操作需要加 @Modifying
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
}
JPQL vs 原生 SQL 的区别:
JPQL: SELECT u FROM User u ← 操作实体类名和字段名
SQL: SELECT * FROM t_user ← 操作数据库表名和列名
推荐用 JPQL,代码和数据库解耦,换数据库也不用改。
五、重头戏:动态查询(Specification)
实际开发中,查询条件往往是动态的——用户可能只填了名字,也可能只选了状态,或者三个条件都填了。
这种场景,方法名查询和 @Query 都不好使了。JPA 的 Specification 就是为此而生的。
5.1 先看问题:没有 Specification 时怎么写?
// 传统写法:一堆 if-else,又臭又长
public List<User> search(String name, Integer minAge, Integer maxAge, UserStatus status) {
// 这种写法有 N 个条件就要 2^N 种组合,根本写不完!
if (name != null && minAge != null && status != null) {
return repo.findByNameAndAgeGreaterThanAndStatus(name, minAge, status);
} else if (name != null && minAge != null) {
return repo.findByNameAndAgeGreaterThan(name, minAge);
} else if (name != null) {
return repo.findByName(name);
}
// ... 疯狂的 if-else
}
痛点:条件越多,组合越爆炸,代码越难维护。
5.2 Specification 登场:动态拼接条件
第一步:让 Repository 继承 JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// 多继承一个接口,就拥有了动态查询能力
}
第二步:编写 Specification
public class UserSpecs {
/**
* 动态查询:根据条件拼接 WHERE 子句
*/
public static Specification<User> dynamicSearch(String name,
Integer minAge,
Integer maxAge,
UserStatus status) {
return (root, query, cb) -> {
// 用于存放所有条件
List<Predicate> predicates = new ArrayList<>();
// 条件1:名字模糊查询(不为空才拼接)
if (StringUtils.hasText(name)) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
// 条件2:最小年龄
if (minAge != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), minAge));
}
// 条件3:最大年龄
if (maxAge != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("age"), maxAge));
}
// 条件4:状态
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
// 把所有条件用 AND 连接 → WHERE 条件1 AND 条件2 AND ...
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
核心原理图解:
┌─────────────────────────────────────────────────────────┐
│ Specification 动态查询 │
│ │
│ 输入条件:name="张", minAge=20, status=null │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ name 不为空? │ │ minAge 不为空│ status 为空,跳过 │
│ │ ✅ 是 │ │ ✅ 是 │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ like(name,"%张%") age >= 20 │
│ │ │ │
│ └────────┬─────────┘ │
│ ▼ │
│ 最终 SQL:WHERE name LIKE '%张%' AND age >= 20 │
│ │
└─────────────────────────────────────────────────────────┘
传了哪个条件,就拼哪个条件,没传的自动跳过。 再也不用写 if-else 了!
第三步:在 Service 中使用
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> search(String name, Integer minAge, Integer maxAge, UserStatus status) {
Specification<User> spec = UserSpecs.dynamicSearch(name, minAge, maxAge, status);
return userRepository.findAll(spec);
}
}
第四步:Controller 调用
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/search")
public List<User> search(@RequestParam(required = false) String name,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(required = false) UserStatus status) {
return userService.search(name, minAge, maxAge, status);
}
}
调用示例:
GET /users/search?name=张&minAge=20
→ WHERE name LIKE '%张%' AND age >= 20
GET /users/search?status=ACTIVE
→ WHERE status = 'ACTIVE'
GET /users/search?name=张&minAge=20&maxAge=30&status=ACTIVE
→ WHERE name LIKE '%张%' AND age >= 20 AND age <= 30 AND status = 'ACTIVE'
GET /users/search
→ 无条件,查询全部
5.3 进阶:多表关联查询
实际项目中,用户通常关联了角色、部门等表。Specification 同样支持:
// 假设 User 关联了 Department(多对一)
@Entity
public class User {
// ... 其他字段
@ManyToOne
@JoinColumn(name = "dept_id")
private Department department;
}
public static Specification<User> withDepartment(String deptName) {
return (root, query, cb) -> {
if (!StringUtils.hasText(deptName)) {
return cb.conjunction(); // 无条件
}
// JOIN 查询:关联 department 表
Join<User, Department> deptJoin = root.join("department", JoinType.LEFT);
return cb.like(deptJoin.get("name"), "%" + deptName + "%");
};
}
5.4 进阶:排序和分页
Specification + 分页排序 = 生产级查询方案:
// 分页 + 排序 + 动态条件,一步到位
public Page<User> searchWithPage(String name,
Integer minAge,
UserStatus status,
int page,
int size) {
Specification<User> spec = UserSpecs.dynamicSearch(name, minAge, null, status);
// 按创建时间倒序
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
// 分页参数(页码从 0 开始)
Pageable pageable = PageRequest.of(page, size, sort);
return userRepository.findAll(spec, pageable);
}
Page 对象包含的信息:
{
"content": [...], // 当前页数据
"totalElements": 100, // 总记录数
"totalPages": 10, // 总页数
"number": 0, // 当前页码
"size": 10, // 每页大小
"first": true, // 是否第一页
"last": false // 是否最后一页
}
前端分页需要的所有信息,一个对象全搞定。
六、完整实战:用户查询接口
把上面的知识串起来,写一个生产可用的查询接口:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
/**
* 用户列表查询(支持多条件 + 分页 + 排序)
*/
@GetMapping
public Page<User> list(UserQueryDTO query) {
// 1. 构建动态查询条件
Specification<User> spec = Specification
.where(StringUtils.hasText(query.getName())
? (root, q, cb) -> cb.like(root.get("name"), "%" + query.getName() + "%")
: null)
.and(query.getMinAge() != null
? (root, q, cb) -> cb.greaterThanOrEqualTo(root.get("age"), query.getMinAge())
: null)
.and(query.getStatus() != null
? (root, q, cb) -> cb.equal(root.get("status"), query.getStatus())
: null);
// 2. 排序
Sort sort = Sort.by(Sort.Direction.fromString(
query.getSortDir() != null ? query.getSortDir() : "DESC"),
query.getSortBy() != null ? query.getSortBy() : "createTime");
// 3. 分页
Pageable pageable = PageRequest.of(
query.getPage() != null ? query.getPage() : 0,
query.getSize() != null ? query.getSize() : 10,
sort);
// 4. 一行搞定查询
return userRepository.findAll(spec, pageable);
}
}
/**
* 查询参数 DTO
*/
@Data
public class UserQueryDTO {
private String name; // 姓名(模糊)
private Integer minAge; // 最小年龄
private Integer maxAge; // 最大年龄
private UserStatus status; // 状态
private String sortBy; // 排序字段
private String sortDir; // 排序方向:ASC / DESC
private Integer page; // 页码(从 0 开始)
private Integer size; // 每页条数
}
七、速查表:JPA 常用注解
实体类注解
| 注解 | 作用 | 示例 |
|---|---|---|
@Entity |
标记为实体类 | @Entity |
@Table |
指定表名 | @Table(name = "t_user") |
@Id |
主键 | @Id |
@GeneratedValue |
主键策略 | @GeneratedValue(strategy = IDENTITY) |
@Column |
字段配置 | @Column(nullable = false, length = 50) |
@Enumerated |
枚举映射 | @Enumerated(EnumType.STRING) |
@CreationTimestamp |
自动填充创建时间 | @CreationTimestamp |
@UpdateTimestamp |
自动填充更新时间 | @UpdateTimestamp |
@ManyToOne |
多对一关系 | @ManyToOne |
@OneToMany |
一对多关系 | @OneToMany(mappedBy = "user") |
@JoinColumn |
外键列 | @JoinColumn(name = "dept_id") |
Repository 查询关键字
| 关键字 | SQL 等价 | 方法名示例 |
|---|---|---|
And |
AND |
findByNameAndAge |
Or |
OR |
findByNameOrEmail |
Like |
LIKE |
findByNameLike |
Containing |
LIKE '%s%' |
findByNameContaining |
GreaterThan |
> |
findByAgeGreaterThan |
LessThan |
< |
findByAgeLessThan |
Between |
BETWEEN |
findByAgeBetween |
In |
IN |
findByStatusIn |
OrderBy |
ORDER BY |
findByNameOrderByAgeDesc |
IsNull |
IS NULL |
findByEmailIsNull |
IsNotNull |
IS NOT NULL |
findByEmailIsNotNull |
Not |
<> |
findByStatusNot |
True / False |
= 1 / = 0 |
findByActiveTrue |
八、常见坑与最佳实践
坑 1:N + 1 查询问题
// ❌ 错误:循环中访问关联对象,每条记录都查一次数据库
List<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user.getDepartment().getName()); // 触发额外查询!
}
// ✅ 正确:使用 JOIN FETCH 一次性加载
@Query("SELECT u FROM User u JOIN FETCH u.department")
List<User> findAllWithDepartment();
坑 2:事务注解别忘加
// ❌ 更新操作不加事务,可能不生效
public void updateUserName(Long id, String name) {
User user = userRepository.findById(id).orElseThrow();
user.setName(name);
userRepository.save(user);
}
// ✅ 加上 @Transactional
@Transactional
public void updateUserName(Long id, String name) {
User user = userRepository.findById(id).orElseThrow();
user.setName(name);
// 不需要显式调用 save(),事务提交时自动更新
}
坑 3:ddl-auto 生产环境必须关
# 开发环境
spring.jpa.hibernate.ddl-auto: update
# 生产环境(用 Flyway / Liquibase 管理表结构)
spring.jpa.hibernate.ddl-auto: none
最佳实践总结
✅ 查询用 JPQL,不直接写 SQL(数据库无关性)
✅ 复杂查询用 Specification,别堆方法名
✅ 列表查询加分页,防止一次拉全量
✅ 关联查询用 JOIN FETCH,避免 N+1
✅ 写操作加 @Transactional
✅ 生产环境 ddl-auto 设为 none
✅ 打印 SQL(show-sql: true)方便调试,上线关掉
九、总结
┌────────────────────────────────────────────┐
│ JPA 技术栈全景 │
│ │
│ JpaRepository │
│ ├── 基本 CRUD(save/delete/findById) │
│ ├── 方法名查询(findBy + 条件) │
│ ├── JPQL 查询(@Query) │
│ └── 动态查询(Specification) ← 本文重点 │
│ │
│ JpaSpecificationExecutor │
│ └── findAll(Specification, Pageable) │
│ ├── 动态条件拼接 │
│ ├── 分页 │
│ └── 排序 │
│ │
│ 一套组合拳,覆盖 90% 的查询场景 │
└────────────────────────────────────────────┘
一句话总结: JPA 让你少写 SQL,Specification 让你少写 if-else。两者配合,查询代码又短又灵活。
如果这篇文章对你有帮助,点个赞收藏一下,后续会更新 Spring Security + JPA 实战 系列。

所有评论(0)