本文适合:刚接触 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

经验之谈ContainingLike 更常用,因为自动加 %,不用手动拼接。

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 实战 系列。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。