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 应用中的大部分数据访问问题。