Spring Data JPA:Repository、Query Method 和 @Query

1. 概述

Spring Data JPA 帮你编写数据访问层,几乎无需代码实现。你只需声明一个接口,Spring 会根据方法名或 @Query 注解自动生成 SQL 语句。

本文涵盖三个主要部分:如何创建基础 repository、query method 的命名规则以便 Spring 自动推导 WHERE 条件,以及何时使用 @Query 来处理复杂的 JPQL/native SQL。读完本文后,你将知道如何为每种实际情况选择正确的方法。

2. 项目配置

添加 Spring Data JPA 和数据库驱动依赖(这里以 H2 为例进行演示):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

贯穿本文的示例实体:

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerEmail;
    private BigDecimal totalAmount;
    private OrderStatus status;
    private LocalDateTime createdAt;

    // getters, setters
}

public enum OrderStatus {
    PENDING, PAID, SHIPPED, CANCELLED
}

Order 有几个简单的字段:客户邮箱、总金额、状态、创建时间。下面的所有查询示例都围绕这个实体展开。

3. 基础 Repository

只需继承 JpaRepository,Spring Boot 在启动时会自动创建 bean 实现:

public interface OrderRepository extends JpaRepository<Order, Long> {
}

JpaRepository<Order, Long> 告诉 Spring 要管理的实体是 Order,主键类型是 Long。这个接口已经提供了 save()、findById()、findAll()、deleteById() 等方法 — 无需额外编写任何代码。

@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Order createOrder(Order order) {
        return orderRepository.save(order);
    }
}

调用 save() 时,如果 id 为 null 则执行 INSERT,否则执行 UPDATE。这是 SimpleJpaRepository 的默认行为 — Spring Data 自动生成的实现。

注意: save() 不是根据业务逻辑来判断“新增”还是“更新”,而是根据 id 是否为 null 来判断。如果你在调用 save() 前手动设置了 id,Spring 会将其视为更新操作,并执行 findById() + merge,导致额外的不必要的 SELECT 查询。

4. Query Method:通过命名规则生成查询

4.1. 基本规则

Spring Data JPA 根据 findBy、existsBy、countBy、deleteBy 等模式来解析方法名,结合字段名和条件关键字:

public interface OrderRepository extends JpaRepository<Order, Long> {

    List<Order> findByStatus(OrderStatus status);

    List<Order> findByCustomerEmailAndStatus(String email, OrderStatus status);

    Optional<Order> findFirstByCustomerEmailOrderByCreatedAtDesc(String email);

    long countByStatus(OrderStatus status);
}
  • findByStatus 生成 WHERE status = ?。
  • findByCustomerEmailAndStatus 用 AND 连接两个条件。
  • findFirstByCustomerEmailOrderByCreatedAtDesc 获取 1 条记录,按 createdAt 降序排列 — 在查找某客户最新订单时很有用。
  • countByStatus 返回数量,而不是列表。

4.2. 比较和范围关键字

public interface OrderRepository extends JpaRepository<Order, Long> {

    List<Order> findByTotalAmountGreaterThan(BigDecimal amount);

    List<Order> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);

    List<Order> findByStatusIn(List<OrderStatus> statuses);

    List<Order> findByCustomerEmailContainingIgnoreCase(String keyword);
}

GreaterThan、Between、In、ContainingIgnoreCase 是 Spring Data JPA 内置的关键字,直接映射到相应的 SQL 子句。ContainingIgnoreCase 生成 LOWER(customer_email) LIKE LOWER('%keyword%') — 这对不区分大小写的搜索很方便。

测试验证行为:

@DataJpaTest
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldFindOrdersByStatus() {
        Order order = new Order();
        order.setStatus(OrderStatus.PAID);
        order.setCustomerEmail("a@test.com");
        order.setTotalAmount(BigDecimal.valueOf(100));
        order.setCreatedAt(LocalDateTime.now());
        orderRepository.save(order);

        List<Order> result = orderRepository.findByStatus(OrderStatus.PAID);

        assertThat(result)
            .hasSize(1)
            .first()
            .extracting(Order::getCustomerEmail)
            .isEqualTo("a@test.com");
    }
}

@DataJpaTest 只加载与 JPA 相关的上下文(repository、entity manager),比 @SpringBootTest 运行更快。默认使用内存数据库,每个测试后都会回滚。

4.3. 分页和排序

public interface OrderRepository extends JpaRepository<Order, Long> {

    Page<Order> findByStatus(OrderStatus status, Pageable pageable);
}
Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending());
Page<Order> page = orderRepository.findByStatus(OrderStatus.PAID, pageable);

System.out.println(page.getTotalElements());
System.out.println(page.getContent());

Pageable 是一个特殊参数 — Spring Data 自动识别它,不会将其视为 WHERE 条件。Page<Order> 返回包含元数据:总元素数、总页数、当前页内容。

4.4. 何时方法名变得复杂

// 难以阅读,容易输入错关键字
List<Order> findByStatusAndCustomerEmailContainingIgnoreCaseAndCreatedAtAfterOrderByTotalAmountDesc(
    OrderStatus status, String keyword, LocalDateTime after);

虽然这样长的方法名仍能运行,但难以阅读、难以维护,而且编译器无法检查语法错误 — Spring Data 只在启动上下文时才会发现错误(fail-fast 但仍然是运行时)。这时应该改用 @Query。

5. @Query:直接控制查询语句

5.1. 基础 JPQL

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.totalAmount > :minAmount")
    List<Order> findHighValueOrders(@Param("status") OrderStatus status,
                                     @Param("minAmount") BigDecimal minAmount);
}

@Query 编写 JPQL — 面向实体的查询语言,而不是纯 SQL(使用 Order o、o.status 而非表名/列名)。@Param 将查询中的参数名映射到方法参数,使变量名灵活,不受顺序限制。

5.2. 需要时使用 Native SQL

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query(value = "SELECT * FROM orders WHERE total_amount > :minAmount LIMIT :limit",
           nativeQuery = true)
    List<Order> findTopOrders(@Param("minAmount") BigDecimal minAmount,
                               @Param("limit") int limit);
}

nativeQuery = true 允许编写原生 SQL,可以使用数据库特定的语法如 LIMIT。代价是牺牲了不同数据库之间的可移植性(例如 Oracle 不支持 LIMIT)。

注意: Native 查询返回实体仍然有效(如果列名与映射匹配),但如果 SELECT 成任意 DTO,你需要添加 @SqlResultSetMapping 或使用 projection 接口 — 比 JPQL 复杂。

5.3. 使用 @Modifying 进行 Update 和 Delete

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Modifying
    @Query("UPDATE Order o SET o.status = :newStatus WHERE o.status = :oldStatus")
    int updateStatusBulk(@Param("oldStatus") OrderStatus oldStatus,
                          @Param("newStatus") OrderStatus newStatus);
}

@Modifying 告诉 Spring Data 这是一条修改数据的语句(UPDATE/DELETE),而不是 SELECT。方法返回 int 表示受影响的行数。缺少 @Modifying 会抛出 InvalidDataAccessApiUsageException,因为 JPA 期望一个 ResultSet 来映射。

注意: 使用 @Modifying 的批量更新直接发往数据库,不会 经过实体生命周期(不触发 @PreUpdate,不更新 persistence context 中的 managed 实体缓存)。如果 service 在内存中持有旧的 Order 实例,内存中的数据将不同步 — 需要在 @Modifying 上设置 clearAutomatically = true 或手动调用 entityManager.clear()。

5.4. Projection:仅获取所需的字段

当不需要整个实体时,使用接口 projection 来减少返回的数据:

public interface OrderSummary {
    Long getId();
    String getCustomerEmail();
    BigDecimal getTotalAmount();
}
public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o.id AS id, o.customerEmail AS customerEmail, o.totalAmount AS totalAmount " +
           "FROM Order o WHERE o.status = :status")
    List<OrderSummary> findSummariesByStatus(@Param("status") OrderStatus status);
}

Spring Data 在运行时创建代理实现 OrderSummary,仅映射 SELECT 出来的 3 个字段。这样可以避免仅为显示摘要列表就加载整个实体 — 减轻 query 和内存的负担。

@Query 的测试:

@DataJpaTest
class OrderRepositoryQueryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldFindHighValueOrders() {
        Order cheap = buildOrder(OrderStatus.PAID, BigDecimal.valueOf(50));
        Order expensive = buildOrder(OrderStatus.PAID, BigDecimal.valueOf(500));
        orderRepository.saveAll(List.of(cheap, expensive));

        List<Order> result = orderRepository.findHighValueOrders(
            OrderStatus.PAID, BigDecimal.valueOf(100));

        assertThat(result)
            .hasSize(1)
            .first()
            .extracting(Order::getTotalAmount)
            .isEqualTo(BigDecimal.valueOf(500));
    }

    private Order buildOrder(OrderStatus status, BigDecimal amount) {
        Order order = new Order();
        order.setStatus(status);
        order.setTotalAmount(amount);
        order.setCustomerEmail("test@test.com");
        order.setCreatedAt(LocalDateTime.now());
        return order;
    }
}

这个测试确认 totalAmount > minAmount 条件正常工作 — 仅返回 500 的订单,排除 50 的订单。

6. Query Method 和 @Query 的对比

评判标准 Query Method @Query
编写速度 简单条件下很快 需要手动编写 JPQL/SQL
支持复杂度 JOIN、子查询受限 灵活,支持所有 JPQL/SQL
可读性 多条件时方法名很长 清晰,逻辑分离
错误检查 上下文启动时报错 上下文启动时报错(JPQL),运行时报错(native SQL)
适用场景 基础 CRUD、1-3 个条件 复杂查询、JOIN、聚合、projection

选择原则:从简单情况下的 query method 开始。当方法名超过 3-4 个条件或需要 JOIN/子查询时,改用 @Query 以获得更好的可读性和控制。

7. 常见错误

在使用命名参数时忘记 @Param:

// 错误:缺少 @Param,Spring 不知道 :status 对应哪个参数
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatusRaw(OrderStatus status);

修复方法:在参数上添加 @Param("status"),或使用 -parameters 编译标志让 Spring 从字节码中自动获取参数名(Spring Boot 默认已在 Maven/Gradle 插件中启用此标志,但仍应明确指定 @Param 以避免依赖构建配置)。

使用 findAll() 然后用 Java stream 过滤,而不是编写查询:

// 不推荐:将整个表加载到内存再过滤
List<Order> paidOrders = orderRepository.findAll().stream()
    .filter(o -> o.getStatus() == OrderStatus.PAID)
    .toList();

这种方法会将整个表的数据从数据库加载到应用程序,浪费带宽和内存。始终应该通过 query method 或 @Query 将筛选条件下推到数据库。

在没有事务的上下文中调用 @Modifying 时忘记 @Transactional:

@Modifying
@Query("DELETE FROM Order o WHERE o.status = :status")
void deleteByStatusBulk(@Param("status") OrderStatus status);

在没有外层事务的情况下直接调用此方法会抛出 TransactionRequiredException。始终要在 service 层的 @Transactional 方法中调用它,不要直接从 controller 调用。

8. 总结

Query method 适合简单查询,当条件不超过 2-3 个字段时,代码简洁易读。当逻辑更复杂时 — 需要 JOIN、子查询、批量更新或自定义 projection — @Query 是更清晰、更易维护的选择。

一般原则:优先将数据过滤逻辑下推到数据库,而不是在 Java 中处理;为每个重要查询编写测试;仅在 JPQL 无法满足时才考虑使用 native SQL(例如需要数据库特定的函数)。掌握这三种技术,你就有足够的工具来处理实际 Spring Boot 应用中的大部分数据访问问题。

spring-bootspring-data-jpajavadatabaseorm