Skip to content

Spring Data JPA 查询

Spring Data JPA 提供方法名查询、JPQL、原生 SQL、Specification 等多种查询方式。选择查询方式时,要同时考虑可读性、动态条件、SQL 可控性和性能。

本页讲查询方式选择。Repository 方法如何被解析、Page 为什么会执行 count、DTO 投影和 Specification 的底层链路见 Spring Data JPA 核心全过程原理

查询方式选择

mermaid
flowchart TD
    A["需要查询"] --> B{"条件是否简单"}
    B -- "是" --> C["方法名查询"]
    B -- "否" --> D{"条件是否动态组合"}
    D -- "是" --> E["Specification"]
    D -- "否" --> F{"是否面向实体查询"}
    F -- "是" --> G["@Query JPQL"]
    F -- "否" --> H["原生 SQL"]

方法名查询

java
List<User> findByStatus(Integer status);

Optional<User> findByUsernameAndStatus(String username, Integer status);

List<User> findByStatusOrderByCreatedAtDesc(Integer status);

适合简单、固定条件查询。

JPQL

JPQL 面向实体和属性,不是直接写表名和字段名。

java
@Query("select u from User u where u.status = :status order by u.createdAt desc")
List<User> findActiveUsers(Integer status);

注意:User 是实体名,createdAt 是实体属性。

原生 SQL

复杂 SQL 或数据库特有语法可以使用原生 SQL:

java
@Query(
    value = "select * from users where status = :status order by created_at desc",
    nativeQuery = true
)
List<User> findByStatusNative(Integer status);

原生 SQL 可控性强,但可移植性较差,也要自己关注字段映射。

DTO 查询

列表接口不建议直接返回 Entity,可以返回 DTO。

java
public record UserListItem(Long id, String username) {
}
java
@Query("select new com.example.UserListItem(u.id, u.username) from User u where u.status = :status")
List<UserListItem> findUserList(Integer status);

DTO 查询能避免暴露实体字段,也能减少懒加载问题。

Specification 动态查询

当查询条件来自页面筛选项,且每个条件都可能为空时,可以用 Specification。

java
public static Specification<User> hasStatus(Integer status) {
    return (root, query, cb) -> {
        if (status == null) {
            return cb.conjunction();
        }
        return cb.equal(root.get("status"), status);
    };
}

组合:

java
Specification<User> spec = Specification.where(hasStatus(status));
Page<User> page = userRepository.findAll(spec, pageable);

N+1 问题

N+1 是 JPA 最常见性能问题之一。

mermaid
flowchart TD
    A["查询 1 次订单列表"] --> B["得到 N 个订单"]
    B --> C["逐个访问 order.user"]
    C --> D["额外查询 N 次用户"]

解决方式:

方式说明
fetch join查询时一次加载关联
EntityGraph声明需要加载的关联
DTO 查询直接查需要字段
分开批量查询先查主表,再批量查关联

分页查询注意

Page 会执行数据查询和 count 查询:

java
Page<User> page = userRepository.findByStatus(status, pageable);

大表 count 可能很慢。如果只需要判断是否有下一页,可以使用 Slice

常见问题

问题原因建议
方法名难读条件太多改用 @Query 或 Specification
查询很慢SQL 不合理或缺索引打印 SQL,看执行计划
序列化报错懒加载或循环引用返回 DTO
N+1列表后访问关联fetch join 或 DTO
count 慢大表分页优化 count 或使用 Slice

小结

Spring Data JPA 查询方式要按复杂度选择:简单查询用方法名,固定复杂查询用 JPQL,数据库特性用原生 SQL,动态条件用 Specification。性能问题要看最终 SQL,不要只看 Java 方法。

查询生成原理

Spring Data JPA 会把 Repository 方法解析成查询模型,再交给 JPA Provider 生成 SQL。

mermaid
flowchart TD
    A["Repository 方法"] --> B["解析方法名或 @Query"]
    B --> C["生成 JPQL / Criteria"]
    C --> D["Hibernate 生成 SQL"]
    D --> E["数据库执行"]

Demo:查询方式选择

java
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);

    @Query("select u from User u where u.status = :status order by u.id desc")
    Page<User> pageByStatus(@Param("status") Integer status, Pageable pageable);
}

简单查询用方法名,复杂查询用 @Query,动态组合条件再考虑 Specification。

继续深入:Spring Data JPA 核心全过程原理

Repository 方法为什么能执行

Repository 接口没有实现类,但 Spring Data JPA 启动时会为它创建代理对象。

mermaid
flowchart TD
    A["启动扫描 Repository 接口"] --> B["读取泛型 Entity 和 ID"]
    B --> C["创建 Repository 代理"]
    C --> D["分析方法签名"]
    D --> E{"方法来源"}
    E -- "内置 CRUD" --> F["调用 SimpleJpaRepository"]
    E -- "方法名查询" --> G["解析方法名"]
    E -- "@Query" --> H["使用 JPQL 或 SQL"]
    E -- "Specification" --> I["生成 Criteria 查询"]
    F --> J["EntityManager 执行"]
    G --> J
    H --> J
    I --> J

所以 Spring Data JPA 不是“凭空查数据库”,它最终仍然通过 EntityManager 和 Hibernate 生成 SQL。

方法名查询的边界

简单方法名很好读:

java
List<Asset> findByStatusOrderByCreatedAtDesc(Integer status);

但复杂方法名会变成灾难:

java
List<Asset> findByHospitalIdAndDeptIdInAndStatusAndAssetNameContainingAndCreatedAtBetweenOrderByCreatedAtDesc(...);

问题:

  1. 方法名过长,难维护。
  2. 条件组合变化时要新增很多方法。
  3. 不容易看最终 SQL。
  4. 查询优化空间有限。

建议:

场景推荐
1 到 2 个固定条件方法名查询
固定复杂查询@Query JPQL
需要数据库特性原生 SQL
多条件动态组合Specification 或 Querydsl
列表接口DTO 投影

JPQL 和原生 SQL 的区别

JPQL 面向实体和属性:

java
@Query("""
    select a
    from Asset a
    where a.status = :status
    order by a.createdAt desc
""")
List<Asset> listByStatus(@Param("status") Integer status);

原生 SQL 面向表和列:

java
@Query(
    value = """
        select *
        from asset
        where status = :status
        order by created_at desc
    """,
    nativeQuery = true
)
List<Asset> listByStatusNative(@Param("status") Integer status);
对比JPQL原生 SQL
写法实体名、属性名表名、字段名
可移植性较好依赖数据库
复杂 SQL一般
返回 DTO支持构造表达式需要映射
调优仍要看生成 SQL更直接

DTO 投影为什么推荐

列表页只需要少量字段,不应该返回完整 Entity。

java
public record AssetListItem(
    Long id,
    String assetCode,
    String assetName,
    String deptName
) {
}
java
@Query("""
    select new com.demo.AssetListItem(
        a.id, a.assetCode, a.assetName, d.deptName
    )
    from Asset a
    join a.department d
    where a.status = :status
    order by a.createdAt desc
""")
List<AssetListItem> listItems(@Param("status") Integer status);

优点:

  1. 只查接口需要字段。
  2. 避免懒加载。
  3. 避免循环引用。
  4. 避免暴露 Entity 内部字段。
  5. 更容易控制 SQL。

Specification 详细 Demo

查询条件:

java
public class AssetQuery {
    private Long hospitalId;
    private Long deptId;
    private Integer status;
    private String assetName;
}

Specification:

java
public class AssetSpecifications {
    public static Specification<Asset> byQuery(AssetQuery query) {
        return (root, cq, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (query.getHospitalId() != null) {
                predicates.add(cb.equal(root.get("hospitalId"), query.getHospitalId()));
            }
            if (query.getDeptId() != null) {
                predicates.add(cb.equal(root.get("deptId"), query.getDeptId()));
            }
            if (query.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), query.getStatus()));
            }
            if (query.getAssetName() != null && !query.getAssetName().isBlank()) {
                predicates.add(cb.like(root.get("assetName"), "%" + query.getAssetName() + "%"));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
}

使用:

java
Page<Asset> page = assetRepository.findAll(
    AssetSpecifications.byQuery(query),
    PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt"))
);

注意:

  1. Specification 可读性比长方法名好。
  2. 复杂 join 和报表查询仍可能不如手写 SQL 清楚。
  3. 要看最终 SQL 和执行计划。
  4. 动态条件要避免无条件全表扫描。

fetch join 和分页的坑

fetch join 可以解决 N+1,但和分页一起用要谨慎,尤其是一对多关联。

java
@Query("""
    select a
    from Asset a
    join fetch a.collectLogs
    where a.status = :status
""")
Page<Asset> pageAssets(Integer status, Pageable pageable);

风险:

  1. 一对多 fetch join 会把一条主记录放大成多行。
  2. 数据库分页可能作用在 join 后结果上,不是主表实体上。
  3. Hibernate 可能内存分页,导致性能灾难。

列表页更推荐 DTO 查询或先分页查主表 ID,再批量查关联数据。

Page 查询为什么可能慢

Page<T> 通常会执行两条 SQL:

  1. 查询当前页数据。
  2. 查询总数 count。
mermaid
flowchart TD
    A["Repository 返回 Page"] --> B["执行数据查询"]
    A --> C["执行 count 查询"]
    B --> D["当前页内容"]
    C --> E["总条数和总页数"]

复杂查询里 count 可能比数据查询还慢。比如大表、多 join、group by、distinct。

解决:

  1. 不需要总数时使用 Slice
  2. 手写简化 countQuery。
  3. 限制筛选条件和最大页码。
  4. 报表走异步统计或预聚合。

查询慢排查

mermaid
flowchart TD
    A["JPA 查询慢"] --> B["打开 Hibernate SQL 日志"]
    B --> C["拿到最终 SQL 和参数"]
    C --> D["数据库 EXPLAIN"]
    D --> E{"是否 N+1"}
    E -- "是" --> F["DTO / fetch join / EntityGraph"]
    E -- "否" --> G{"count 是否慢"}
    G -- "是" --> H["Slice 或手写 countQuery"]
    G -- "否" --> I["检查索引、排序、分页、锁等待"]

面试标准回答

text
Spring Data JPA 查询方式包括方法名查询、JPQL、原生 SQL、Specification、DTO 投影等。简单固定条件可以用方法名查询,复杂固定查询用 @Query JPQL,需要数据库特性时用原生 SQL,多条件动态组合可用 Specification。Repository 接口运行时由 Spring Data 创建代理,最终仍然通过 EntityManager 和 Hibernate 生成 SQL。列表接口不建议返回 Entity,优先 DTO 投影,避免懒加载、循环引用和多余字段。Page 会执行数据查询和 count 查询,大表或复杂查询不需要总数时可用 Slice。