当前位置: 首页 > news >正文

Spring Data JPA @Query注解

1. 概述

Spring Data提供了许多方法来定义我们可以执行的查询。其中之一是@Query注释。

在本教程中,我们将演示如何使用春季数据JPA中的@Query注释来执行JPQL和本机SQL查询。

我们还将展示在@Query注释不够用时如何构建动态查询。

延伸阅读:

春季数据 JPA 存储库中的派生查询方法

探索春季数据 JPA 中的查询派生机制。

春季数据 JPA @Modifying注释

通过组合@Query和@Modifying注释,在春季数据 JPA 中创建 DML 和 DDL 查询

2. 选择“查询”

为了定义要为Spring数据存储库方法执行的SQL,我们可以使用@Query注释来注释该方法 - 其属性包含要执行的JPQL或SQL。

@Query批注优先于命名查询,命名查询使用@NamedQuery进行批注或在 orm.xml 文件中定义。

将查询定义放在存储库内的方法正上方,而不是作为命名查询放在域模型内部,这是一种很好的方法。存储库负责持久性,因此它是存储这些定义的更好地方。

2.1. JPQL

默认情况下,查询定义使用 JPQL。

让我们看一个简单的存储库方法,该方法从数据库中返回活动的 User 实体:

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsers();

2.2. 原生

我们也可以使用本机 SQL 来定义我们的查询。我们所要做的就是将本机Query属性的值设置为true,并在注释的值属性中定义本机SQL查询:

@Query(
  value = "SELECT * FROM USERS u WHERE u.status = 1", 
  nativeQuery = true)
Collection<User> findAllActiveUsersNative();

3. 在查询中定义顺序

我们可以将 Sort 类型的附加参数传递给具有@Query批注的 Spring Data 方法声明。它将被转换为传递给数据库的 ORDER BY 子句。

3.1. JPA 提供的方法和派生方法的排序

对于我们开箱即用的方法,例如 findAll(Sort) 或通过解析方法签名生成的方法,我们只能使用对象属性来定义我们的排序

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));

现在想象一下,我们要按 name 属性的长度进行排序:

userRepository.findAll(Sort.by("LENGTH(name)"));

当我们执行上述代码时,我们将收到一个异常:

org.springframework.data.mapping.PropertyReferenceException: No property LENGTH(name) found for type User!

3.2. JPQL

当我们使用JPQL进行查询定义时,Spring Data可以毫无问题地处理排序 - 我们所要做的就是添加一个Sort类型的方法参数:

@Query(value = "SELECT u FROM User u")
List<User> findAllUsers(Sort sort);

我们可以调用此方法并传递 Sort 参数,该参数将按 User 对象的 name 属性对结果进行排序:

userRepository.findAllUsers(Sort.by("name"));

由于我们使用了@Query注释,因此我们可以使用相同的方法来按名称的长度获取排序的用户列表:

userRepository.findAllUsers(JpaSort.unsafe("LENGTH(name)"));

使用 JpaSort.unsafe() 创建排序对象实例至关重要。

当我们使用:

Sort.by("LENGTH(name)");

然后,我们将收到与上面看到的 findAll() 方法完全相同的异常。

当Spring Data发现使用@Query注释的方法的不安全排序顺序时,它只会将排序子句附加到查询中 - 它会跳过检查排序依据的属性是否属于域模型。

3.3. 原生

@Query批注使用本机 SQL 时,则无法定义排序

如果我们这样做,我们将收到一个例外:

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination

正如异常情况所说,本机查询不支持排序。错误消息提示分页也会导致异常。

但是,有一种启用分页的解决方法,我们将在下一节中介绍它。

4. 分页

分页允许我们在主页中仅返回整个结果的子集。例如,在网页上的多个数据页面中导航时,这很有用。

分页的另一个优点是,从服务器发送到客户端的数据量是最小的。通过发送较小的数据片段,我们通常可以看到性能的提高。

4.1. JPQL

在 JPQL 查询定义中使用分页非常简单:

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPagination(Pageable pageable);

我们可以传递一个页面请求参数来获取一页数据。

本机查询也支持分页,但需要一些额外的工作。

4.2. 原生

我们可以通过声明其他属性 countQuery 来为本机查询启用分页。

这定义了要执行的 SQL,以计算整个结果中的行数:

@Query(
  value = "SELECT * FROM Users ORDER BY id", 
  countQuery = "SELECT count(*) FROM Users", 
  nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

4.3. 2.0.4 之前的弹簧数据JPA版本

上述本机查询解决方案适用于Spring数据JPA版本2.0.4及更高版本。

在该版本之前,当我们尝试执行此类查询时,我们将收到我们在上一节有关排序中描述的相同异常。

我们可以通过在查询中添加一个额外的分页参数来克服这个问题:

@Query(
  value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
  countQuery = "SELECT count(*) FROM Users",
  nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

在上面的例子中,我们添加

\n-- #pageable\n

作为分页参数的占位符。这告诉春季数据JPA如何解析查询并注入可分页参数。此解决方案适用于 H2 数据库。

我们已经介绍了如何通过 JPQL 和本机 SQL 创建简单的选择查询。接下来,我们将演示如何定义其他参数。

5. 索引查询参数

有两种可能的方法可以将方法参数传递给查询:索引参数和命名参数。

在本节中,我们将介绍索引参数。

5.1. JPQL

对于 JPQL 中的索引参数,Spring Data 将按照方法声明中出现的顺序将方法参数传递给查询

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatus(Integer status);

@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndName(Integer status, String name);

对于上述查询,状态方法参数将分配给索引为 1 的查询参数,并将 name 方法参数分配给索引为 2 的查询参数。

5.2. 原生

本机查询的索引参数的工作方式与 JPQL 完全相同:

@Query(
  value = "SELECT * FROM Users u WHERE u.status = ?1", 
  nativeQuery = true)
User findUserByStatusNative(Integer status);

在下一节中,我们将展示一种不同的方法:通过 name 传递参数。

6. 命名参数

我们还可以使用命名参数将方法参数传递给查询。我们使用存储库方法声明中的@Param注释来定义这些内容。

用 @Param 注释的每个参数都必须具有与相应的 JPQL 或 SQL 查询参数名称匹配的值字符串。具有命名参数的查询更易于阅读,并且在需要重构查询时不易出错。

6.1. JPQL

如上所述,我们在方法声明中使用@Param注释,以将JPQL中按名称定义的参数与方法声明中的参数进行匹配:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParams(
  @Param("status") Integer status, 
  @Param("name") String name);

请注意,在上面的示例中,我们将 SQL 查询和方法参数定义为具有相同的名称,但只要值字符串相同,就不需要它:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByUserStatusAndUserName(@Param("status") Integer userStatus, 
  @Param("name") String userName);

6.2. 原生

对于本机查询定义,与 JPQL 相比,我们如何通过名称将参数传递给查询没有区别 — 我们使用@Param注释:

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name", 
  nativeQuery = true)
User findUserByStatusAndNameNamedParamsNative(
  @Param("status") Integer status, @Param("name") String name);

7. 集合参数

让我们考虑一下 JPQL 或 SQL 查询的 where 子句包含输入(或非 IN)关键字的情况:

SELECT u FROM User u WHERE u.name IN :names

在这种情况下,我们可以定义一个将 Collection 作为参数的查询方法:

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameList(@Param("names") Collection<String> names);

由于参数是集合,因此可以与列表、哈希集等一起使用。

接下来,我们将演示如何使用 @修改注释修改数据。

8. 使用@Modifying更新查询

我们可以使用 @Query 注释来修改数据库的状态,方法是将 @Modifying 注释添加到存储库方法中。

8.1. JPQL

选择查询相比,修改数据的存储库方法有两个区别 — 它具有@Modifying注释,当然,JPQL 查询使用 update 而不是 select

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status, 
  @Param("name") String name);

返回值定义执行查询时更新的行数。索引参数和命名参数都可以在更新查询中使用。

8.2. 原生

我们也可以使用本机查询来修改数据库的状态。我们只需要添加@Modifying注释:

@Modifying
@Query(value = "update Users u set u.status = ? where u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

8.3. 插入

要执行插入操作,我们必须应用@Modifying并使用本机查询,因为 INSERT 不是 JPA 接口的一部分:

@Modifying
@Query(
  value = 
    "insert into Users (name, age, email, status) values (:name, :age, :email, :status)",
  nativeQuery = true)
void insertUser(@Param("name") String name, @Param("age") Integer age, 
  @Param("status") Integer status, @Param("email") String email);

9. 动态查询

通常,我们会遇到基于条件或数据集构建SQL语句的需要,这些条件或数据集的值仅在运行时才知道。在这些情况下,我们不能只使用静态查询。

9.1. 动态查询示例

例如,让我们想象一种情况,我们需要从运行时定义的集合中选择所有电子邮件类似于一个的用户 - email1email2,...,emailn

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
    or  u.email LIKE '%email2%'
    ... 
    or  u.email LIKE '%emailn%'

由于集合是动态构造的,因此我们无法在编译时知道要添加多少 LIKE 子句。

在这种情况下,我们不能只使用@Query注释,因为我们无法提供静态 SQL 语句。

相反,通过实现自定义复合存储库,我们可以扩展基本的 JpaRepository 功能,并提供我们自己的逻辑来构建动态查询。让我们来看看如何做到这一点。

9.2. 自定义仓库和 JPA 标准 API

对我们来说幸运的是,Spring提供了一种通过使用自定义片段接口来扩展基本存储库的方法。然后,我们可以将它们链接在一起以创建复合存储库。

我们将从创建自定义片段接口开始:

public interface UserRepositoryCustom {
    List<User> findUserByEmails(Set<String> emails);
}

然后我们将实现它:

public class UserRepositoryCustomImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findUserByEmails(Set<String> emails) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);

        Path<String> emailPath = user.get("email");

        List<Predicate> predicates = new ArrayList<>();
        for (String email : emails) {
            predicates.add(cb.like(emailPath, email));
        }
        query.select(user)
            .where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

        return entityManager.createQuery(query)
            .getResultList();
    }
}

如上所示,我们利用 JPA 标准 API 来构建动态查询。

此外,我们需要确保在类名中包含 Impl 后缀。Spring 将搜索“用户存储库自定义”实现为“用户存储库自定义”。由于片段本身不是存储库,因此Spring依靠这种机制来查找片段实现。

9.3. 扩展现有仓库

请注意,从第 2 节到第 7 节的所有查询方法都在用户存储库中。

因此,现在我们将通过在用户存储库中扩展新界面来集成我们的片段:

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
    //  query methods from section 2 - section 7
}

9.4. 使用仓库

最后,我们可以调用我们的动态查询方法:

Set<String> emails = new HashSet<>();
// filling the set with any number of items

userRepository.findUserByEmails(emails);

我们已成功创建了一个复合存储库,并调用了我们的自定义方法。

10. 结论

在本文中,我们介绍了使用@Query注释在Spring数据JPA存储库方法中定义查询的几种方法。

我们还学习了如何实现自定义存储库和创建动态查询。

与往常一样,本文中使用的完整代码示例可在 GitHub 上找到。

相关文章:

  • 猿创征文|Python快速刷题网站——牛客网 数据分析篇(十四)
  • 【元宇宙】元宇宙的定义、特征、要素及架构
  • 4、“组件协作“模式
  • 刚开始做自媒体,无从下手,有什么好的建议吗?
  • 2022 华为杯研赛F题思路 研究生数学建模
  • Opencv项目实战:12 你这背景太假啦!
  • python解CCF-CSP真题《202209-1 如此编码》
  • 数据分析可视化08 案例 2:历史数据变化趋势图设计
  • Redis-缓存击穿
  • 信息学奥赛一本通:2072:【例2.15】歌手大奖赛
  • 【Linux】进程控制 (万字)
  • ARMv9新特性:虚拟内存系统架构 (VMSA) 的增强功能
  • 【JavaSE】之流程控制与方法
  • SpringCloud——网关1
  • 『Android基础入门』ViewPager+Fragment+BottomNavigationView实现底部导航
  • AngularJS指令开发(1)——参数详解
  • el-input获取焦点 input输入框为空时高亮 el-input值非法时
  • iOS仿今日头条、壁纸应用、筛选分类、三方微博、颜色填充等源码
  • JavaScript-Array类型
  • JavaScript服务器推送技术之 WebSocket
  • jdbc就是这么简单
  • JS题目及答案整理
  • k8s 面向应用开发者的基础命令
  • linux安装openssl、swoole等扩展的具体步骤
  • mysql外键的使用
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • 聚簇索引和非聚簇索引
  • 利用DataURL技术在网页上显示图片
  • 入职第二天:使用koa搭建node server是种怎样的体验
  • 项目管理碎碎念系列之一:干系人管理
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • (C#)获取字符编码的类
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (ZT)薛涌:谈贫说富
  • (二)PySpark3:SparkSQL编程
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)EXC_BREAKPOINT僵尸错误
  • (转)Unity3DUnity3D在android下调试
  • (转)大型网站的系统架构
  • (转载)深入super,看Python如何解决钻石继承难题
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • .gitignore文件设置了忽略但不生效
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .net访问oracle数据库性能问题
  • .NET简谈设计模式之(单件模式)
  • .NET企业级应用架构设计系列之技术选型
  • [383] 赎金信 js
  • [BZOJ]4817: [Sdoi2017]树点涂色
  • [C++]C++基础知识概述
  • [Effective C++读书笔记]0012_复制对象时勿忘其每一部分
  • [GN] Vue3.2 快速上手 ---- 核心语法2