Ohhnews

分类导航

$ cd ..
Baeldung原文

解决 Spring Data JPA 中的查询验证失败错误

#spring data jpa#数据库查询#后端开发#异常处理#软件测试

1. 引言

在使用 Spring Data JPA 时,我们经常依赖 @Query 注解来定义自定义的 JPQL 或原生 SQL 查询。然而,开发者常会遇到一个令人困扰的问题,即在应用程序启动时抛出以下异常:

$ java
Caused by: java.lang.IllegalArgumentException: Validation failed for query for method ...

此错误是 Spring Data JPA 的一种“快速失败”(fail-fast)机制。它会在应用程序上下文加载时立即尝试验证我们的查询语句,从而防止运行时出现故障。在本教程中,我们将探讨导致此验证错误的常见根本原因,并提供相应的解决方案。

2. 理解根本原因

自定义查询的验证是 Spring Data JPA 存储库(Repository)初始化生命周期的核心部分。通过在启动时验证每个 @Query 声明的语法,框架提供了一个保护层,确保数据库交互在应用程序处理第一个请求之前,其结构是健全的。

2.1. SimpleJpaQuery 的作用

根据常见的错误堆栈信息,负责此验证的组件是 org.springframework.data.jpa.repository.query.SimpleJpaQuery。当 ApplicationContext 启动时,Spring 会扫描应用程序中的存储库接口。对于每一个 @Query 注解,它都会调用 validateQuery() 方法。

2.2. 为什么抛出 IllegalArgumentException

使用 IllegalArgumentException 并非偶然。根据 JPA 规范,如果查询字符串被判定为无效,EntityManager.createQuery() 方法必须抛出 IllegalArgumentException

当 JPA 提供程序因拼写错误或缺失实体而无法解析 JPQL 时,它会抛出此异常。随后,Spring Data JPA 会捕获该异常,并将其包装在一个描述性消息中,明确指出是哪个存储库方法出现了问题。

3. 常见陷阱与解决方案

让我们检查导致此验证错误的三种最常见原因及其修复方法。首先,请看以下 User 实体:

$ java
@Entity
@Table(name = "users")
public class User {
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "group")
    private String group;
    private Integer status;
}

我们将使用 @DataJpaTest 来验证我们的解决方案。这个专门的测试注解会在上下文初始化期间触发 SimpleJpaQuery.validateQuery() 方法,确保我们的查询在测试运行前结构是正确的。

3.1. 表名或列名中的保留关键字

验证失败最常见的原因之一是在没有正确转义的情况下使用了 SQL 保留关键字,如 ORDERGROUP。例如,在以下查询中,我们引用了 group 列:

$ java
@Query("SELECT u FROM User u WHERE u.group = :groupName")
List<User> findByGroup(@Param("groupName") String groupName);

大多数 SQL 方言会因为 GROUPGROUP BY 子句的一部分而导致解析失败。要修复此问题,我们需要在实体定义中对列名进行转义:

$ java
@Column(name = "`group`") 
private String group;

在实体中正确转义列名后,以下测试验证了 ApplicationContext 可以正常加载,且查询可以成功执行:

$ java
@DataJpaTest
@ActiveProfiles("h2")
class UserRepositoryIntegrationTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void givenUser_whenFindByGroup_thenReturnsUser() {
        User user = new User();
        user.setGroup("Admin");
        userRepository.save(user);
        
        // 验证转义后的 'group' 标识符在 JPQL 中有效
        List<User> result = userRepository.findByGroup("Admin");
        
        assertEquals(1, result.size());
        assertEquals("Admin", result.get(0).getGroup());
    }
}

3.2. 实体属性不匹配

JPQL 对实体名称和属性是区分大小写的,因为它查询的是 Java 对象而非数据库表。一个常见的错误是使用数据库列名而不是 Java 字段名。如果我们使用 first_name 编写查询,验证将会失败:

$ java
// User 实体中不存在 first_name
@Query("SELECT u FROM User u WHERE u.first_name = :name")

正确的做法始终是使用 Java 字段标识符:

$ java
@Query("SELECT u FROM User u WHERE u.firstName = :name")

通过使用 Java 字段名,Spring Data JPA 可以在启动期间成功地将查询映射到实体:

$ java
@Test
void givenUser_whenFindByFirstName_thenReturnsUser() {
    User user = new User();
    user.setFirstName("John");
    userRepository.save(user);
    
    // 验证 JPQL 正确引用了 Java 的 'firstName' 属性
    List<User> result = userRepository.findByFirstName("John");
        
    assertEquals(1, result.size());
    assertEquals("John", result.get(0).getFirstName());
}

3.3. nativeQuery 标志不匹配

如果我们编写标准的 SQL 来引用表名或列名,但没有将 nativeQuery 标志设置为 true,JPA 解析器会尝试将其解释为 JPQL,从而导致失败:

$ java
// 解析器会寻找名为 'users' 的实体,而非数据库表
@Query("SELECT * FROM users WHERE status = 1")

我们需要添加 nativeQuery 标志以避免错误:

$ java
@Query(value = "SELECT * FROM users WHERE status = 1", nativeQuery = true)

以下测试验证了原生 SQL 执行可由底层数据库驱动程序正确处理:

$ java
@Test
void givenActiveUser_whenFindActiveUsers_thenReturnsUser() {
    User user = new User();
    user.setFirstName("Jane");
    user.setStatus(1);
    userRepository.save(user);
    
    // 通过 nativeQuery = true 验证原生 SQL 执行
    List<User> result = userRepository.findActiveUsers();
        
    assertEquals(1, result.size());
    assertEquals(1, result.get(0).getStatus());
}

上述测试之所以有效,是因为 Spring Data JPA 在创建存储库代理时会验证 @Query 字符串。通过使用 @DataJpaTest,我们为 UserRepository 中的每个方法调用了 SimpleJpaQuery.validateQuery()

3.4. 修正后的实体与存储库

在上述章节中,我们讨论了常见的陷阱及其解决方案。现在,我们来看一下更新后的实体和存储库:

$ java
@Entity
@Table(name = "users")
public class User {
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "`group`")
    private String group;
    private Integer status;
}

修正后的 UserRepository 如下所示:

$ java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.group = :group")
    List<User> findByGroup(@Param("group") String group);

    @Query("SELECT u FROM User u WHERE u.firstName = :firstName")
    List<User> findByFirstName(@Param("firstName") String firstName);

    @Query(value = "SELECT * FROM users WHERE status = 1", nativeQuery = true)
    List<User> findActiveUsers();
}

这些更改使得 ApplicationContext 能够顺利加载,因为所有的查询验证现在都能成功通过。

4. 结论

在本教程中,我们了解到“Validation failed for query for method”错误是 Spring Data JPA 的一项保护性功能。我们研究了导致此异常的常见陷阱,以及在编写数据库查询时如何规避这些问题。为了更好地理解,我们使用了 @DataJpaTest 注解进行测试,该注解配置了一个内存数据库,并在上下文初始化期间验证存储库查询。