Spring Data JPA - @Query注解指南

2,039 阅读9分钟

简介

如果你已经用 春天数据JPA工作过一段时间,你可能已经熟悉了派生查询方法

public interface MyRepository extends JpaRepository<Entity, Long> {
   List<Entity> findByOrganizationName(String name);
}

它们是一种灵巧而快速的方式,通过简单地定义方法名,就可以减轻Spring Data JPA编写查询的负担。

在这个假想的场景中,我们为一个Entity 类定义了一个JpaRepository ,该类有一个名为organizationName 的属性。而不是在实现MyRepository 的服务中实现这个方法--Spring Data JPA会根据方法的名称自动生成一个查询。它将生成一个查询,返回所有Entity 记录的列表,其中有一个匹配的organizationName

一旦该方法被调用,就会发出以下Hibernate请求。

select 
client0_.id as id1_0_, client0_.age as age2_0_, client0_.name 
as 
name3_0_, client0_.organinzation as organinz4_0_ 
from 
client client0_ where client0_.organinzation=?

这是Spring Data JPA的一个极其灵活和强大的功能,它允许你 bootstrap查询,而无需编写查询本身,甚至无需在后端实现任何处理逻辑。

然而,当需要复杂的查询时,它们会变得非常难以创建。

public interface PropertyRepository extends JpaRepository<Property, Long> {
   List<Property> findPropertiesByTransactionTypeAndPropertyType(@Param("transaction_type") TransactionType transactionType, @Param("property_type") PropertyType propertyType);
}

而这仅仅是针对_两个_参数。当你想创建一个5个参数的查询时会发生什么?

另外,你要创建多少种方法变化

这时你很可能想自己编写查询。这可以通过@Query 注解来实现。

@Query 注解适用于JpaRepository 接口中的方法级别,并与单个方法相关。注解中使用的语言取决于你的后端,或者你可以使用中性的 JPQL(用于关系型数据库)。)

JpaRepository 变体的情况下,比如MongoRepository ,自然,你会写Mongo查询,而如果你使用的是关系型数据库,你会写SQL查询。

当方法被调用时,@Query 注解中的查询就会启动并返回结果。

**注意:**本指南将涵盖Spring Data JPA与关系型数据库的耦合,并将使用JPQL和本地SQL,这不适用于非关系型数据库。

如果您想了解更多关于编写MongoDB原生查询的信息,请阅读我们的@Query注释与MongoDB指南(即将发布!)。

什么是JPQL?

JPQL代表的是 Java持久性查询语言.它被定义在JPA规范中,是一种_面向对象的查询语言_,用于对持久化实体进行数据库操作。

JPA作为调解人,将_JPQL查询_转换为_SQL查询_来执行。

**注意:**应该注意的是,与本地SQL相比,JPQL不与数据库表、记录和字段交互,而是与Java类和实例交互。

它的一些特点包括。

  • 它是一种与平台无关的查询语言。
  • 它简单而稳健。
  • 它可以与任何类型的关系数据库一起使用。
  • 它可以被静态地声明为元数据,也可以动态地建立在代码中。
  • 它是不分大小写的。

如果你的数据库可以改变或从开发到生产的变化,只要它们都是关系型的--JPQL就能创造奇迹,你可以写JPQL查询来创建通用逻辑,可以反复使用。

如果你还不熟悉JPQL,请阅读我们的《理解JPQL指南》(即将推出!)。

如果你使用的是非关系型数据库,如MongoDB--你将编写该数据库的原生查询。

同样,如果你想阅读更多关于编写MongoDB原生查询的信息,请阅读我们的@Query Annotation与MongoDB指南(即将推出!)。

JPQL查询结构

JPQL的语法与SQL的语法非常相似。由于大多数开发人员已经熟悉了SQL的语法,所以学习和使用JPQL变得很容易。

JPQL的SELECT,UPDATEDELETE 查询的结构是。

SELECT ... FROM ...
[WHERE ...]
[GROUP BY ... [HAVING ...]]
[ORDER BY ...]

DELETE FROM ... [WHERE ...]

UPDATE ... SET ... [WHERE ...]

我们稍后将在@Query 注解中手动编写这些查询。

了解@查询注解

@Query 注解只能用于注释资源库接口方法。对注解方法的调用将触发其中发现的语句的执行,它们的用法是非常直接的。

@Query 注解同时支持本地 SQL 和 JPQL。当使用本地SQL时,注解的nativeQuery 参数应该被设置为true

@Query("NATIVE_QUERY...", nativeQuery=true)
List<Entity> findAllByName(String name);

要从数据库中选择所有客户,我们可以使用本地查询或JPQL。

@Query("SELECT(*) FROM CLIENT", nativeQuery=true)
List<Client> findAll();

@Query("SELECT client FROM Client client")
List<Client> findAll();

**就这样了。**你的工作完成了--类似于派生查询方法为你处理工作的方式--当你调用findAll() 方法时,这个@Query 就会启动。

不过,如果你只想找到所有的记录,使用派生查询方法会更容易一些--这就是它们存在的意义。当你想把动态变量作为参数传递给查询本身时,你会想写自己的查询。

引用方法参数

@Query 注释中的本地查询和JPQL查询都可以接受 注释的方法参数,它进一步分类为。

  • 基于位置的参数
  • 命名的参数

当使用基于位置的参数时,你必须跟踪你提供参数的顺序。

@Query("SELECT c FROM Client c WHERE c.name = ?1 AND c.age = ?2")
List<Client> findAll(String name, int age);

传递给方法的第一个参数被映射到?1 ,第二个被映射到?2 ,等等。如果你不小心调换了这些参数--你的查询可能会抛出一个异常,或者默默地产生错误的结果。

另一方面,被命名的参数被命名的,无论其位置如何,都可以通过名称来引用。

@Query("SELECT c FROM Client c WHERE c.name = :name and c.age = :age")
List<Client> findByName(@Param("name") String name, @Param("age") int age);

@Param 注解中的名称与@Query 注解中的命名参数相匹配,所以你可以自由地调用你的变量,但为了保持一致性,我们建议使用相同的名称。

如果你没有为查询中的命名参数提供一个匹配的@Param ,那么在编译时就会抛出一个异常。

@Query("SELECT c FROM Client c WHERE c.name = :name and c.age = :age")
List<Client> findByName(@Param("name") String name, @Param("num1") int age);

结果是。

java.lang.IllegalStateException: Using named parameters for method public abstract ClientRepository.findByName(java.lang.String,int) but parameter 'Optional[num1]' not found in annotated query 'SELECT c FROM Client c WHERE c.name = :name and c.age = :age'!

带有_@Query_注解的SpEL表达式

SpEL (Spring Expression Language)是一种支持查询的语言,并在运行时对对象图进行操作。

#{expression}

可以在@Query 注释中使用的最有用的SpEL表达式是**#{#entityname} 。**

如其名所示,它表示你所在的资源库所引用的实体名称。它避免了说明实际的实体名称,并按如下方式解析。

如果域类型在@Entity 注解上设置了名称属性,那么它就被使用。否则,将使用域类型的简单类名。

换句话说。

// #{#entityName} resolves to "client_entity"
@Entity(name = "client_entity")
public class Client {}

// #{#entityName} resolves to "Client"
@Entity()
public class Client {}

这使得我们有可能将查询抽象为。

public interface ClientRepository extends JpaRepository<Client, Long> {     
    @Query("select e from #{#entityName} e where e.name = ?1")     
    List<Client> findByName(String name);
}

修改查询

DELETEUPDATE 查询被称为_修改性查询_,必须携带一个额外的注解:@Modifying 。这将触发@Query 注释中的查询,使其能够对实体进行修改,而不仅仅是检索数据。

如果没有额外的@Modifying 注解,我们就会面对一个InvalidDataAccessApiUsageException ,让我们知道@Query 注解不支持 DML(数据操作语言语句。

@Modifying
@Query(“DELETE c FROM Client c WHERE c.name = :name”)
void deleteClientByName(@Param("name") String name);

@Modifying
@Query(“UPDATE Client c WHERE c.id = :id”)
void updateUserById(@Param("id") long id);

排序和分页

排序和分页都可以用与派生查询相同的方式进行。

要创建PageableSortable查询,你要向方法提供Pageable 参数,如果你至少扩展了PagingAndSortingRepository 接口,Spring Data JPA就会自动拾取这个参数。

JpaRepository 已经扩展了 PagingAndSortingRepository ,所以该功能是固有的。

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    @Query("select e from #{#entityName} e where e.organization = ?1")
    Page<Client> findByOrganization(String name, Pageable pageable);
}

方法的返回类型变成了Page<T>List<T>Slice<T> ,不过,Page<T> 是唯一一个可以跟踪所有检索到的实体和分页结果的方法。我们现在应该传入的Pageable ,被构造成一个PageRequest.of(int page, int size, Sort sort)

我们提供我们正在寻找的_页面_,页面的大小以及里面的数据如何排序。

@RestController
public class Controller {

    @Autowired
    private ClientRepository clientRepository;

    @GetMapping(value = "/", produces = MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity main() {
        clientRepository.save(new Client(1, 22, "David", "StackAbuse"));
        clientRepository.save(new Client(2, 34, "John", "StackAbuse"));
        clientRepository.save(new Client(3, 46, "Melissa", "StackAbuse"));

        Pageable pageRequest = PageRequest.of(0, 10, Sort.by("age").descending());

        Page<Client> clientPage = clientRepository.findByOrganization("StackAbuse", pageRequest);

        List<Client> clientList = clientPage.getContent();
        int pageNum = clientPage.getNumber();
        long numOfClients = clientPage.getTotalElements();
        long totalNumOfPages = clientPage.getTotalPages();

        return ResponseEntity.ok(
                String.format("Clients: %s, \nCurrent page: %s out of %s, \nTotal entities: %s", 
                clientList, 
                pageNum, 
                totalNumOfPages, 
                numOfClients));
    }

如果我们看一下日志,我们会看到Hibernate查询的启动,以及它们是如何被转换的。

2021-08-13 23:16:06.701 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.727 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.732 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.733 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.734 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.735 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.754 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_, client0_.age as age2_0_, client0_.name as name3_0_, client0_.organinzation as organinz4_0_ from client client0_ where client0_.organinzation=? order by client0_.age desc limit ?

点击REST端点会导致。

Clients: [Client{id=3, age=46, name='Melissa', organization='StackAbuse'}, Client{id=2, age=34, name='John', organization='StackAbuse'}, Client{id=1, age=22, name='David', organization='StackAbuse'}], 
Current page: 0 out of 1, 
Total entities: 3

**注意:**你也可以在查询本身中对数据进行排序,然而,在某些情况下,简单地使用动态输入的Sort 对象会更容易。

总结

派生查询方法是Spring Data JPA的一个伟大的查询约束功能,但其简单性是以可扩展性为代价的。尽管很灵活,但对于扩展到复杂的查询来说,它们并不理想。

这就是Spring Data JPA的@Query 注解发挥作用的地方。

在本指南中,我们看了一下@Query 注解,以及如何在基于Spring的应用程序中利用它来为你的存储库编写自定义的本地和JPQL查询。

我们探讨了参数化选项,以及如何对数据进行分页和排序。