前言

Hibernate 6 是世界上最流行、功能最丰富的 ORM 解决方案的重大重新设计。重新设计涉及 Hibernate 的几乎每个子系统,包括 API、映射注解以及最重要的查询语言。

这是 Hibernate 查询语言第二次从头开始彻底重新实施,但这是十五年多来的第一次。此新版本中,HQL 更加强大,HQL 编译器也更加健壮。

经过漫长的等待,HQL 终于拥有了一组特性,与现代 SQL 方言相匹配,能够充分利用现代 SQL 数据库的功能。

本文档是该语言完整特性集的参考指南,是希望了解如何在 Hibernate 6 中有效编写 HQL 的人的唯一最新来源。

如果您不熟悉 Hibernate,请务必先阅读Hibernate 简介或查看快速入门

1. 基本概念

本文档介绍了 Hibernate 查询语言 (HQL),正如我们可以说的那样,它是一种 Java(现为 Jakarta)持久性查询语言 (JPQL) 的“方言”。

还是相反?

JPQL 是受 HQL 早期版本启发,并且是现代 HQL 的适当子集。这里我们重点描述当今完备的、更强大的 HQL 语言。

如果您正在寻找严格的 JPA 合规性,请使用设置hibernate.jpa.compliance.query=true。使用此配置,任何尝试使用 HQL 特性(超出 JPQL 子集)都将导致异常。

我们不建议使用此设置。

事实上,HQL 现在具有远超纯 JPQL 的功能。我们不必过于纠结于不再将自己局限于这里的标准。在编写特定于数据库的原生 SQL 或独立于数据库的 HQL 之间做出选择时,我们知道自己的偏好是什么。

1.1. HQL 与 SQL

在整个文档中,我们假设您了解 SQL 和关系模型,至少在基本层面上。HQL 和 JPQL 松散地基于 SQL,并且对于熟悉 SQL 的任何人来说都容易学习。

例如,如果您理解此 SQL 查询

select book.title, pub.name            /* projection */
from Book as book                      /* root table */
    join Publisher as pub              /* table join */
        on book.publisherId = pub.id   /* join condition */
where book.title like 'Hibernate%'     /* restriction (selection) */
order by book.title                    /* sorting */

那么我们相信您已经可以理解此 HQL

select book.title, pub.name            /* projection */
from Book as book                      /* root entity */
    join book.publisher as pub         /* association join */
where book.title like 'Hibernate%'     /* restriction (selection) */
order by book.title                    /* sorting */

您可能会注意到,即使对于这个非常简单的示例,HQL 版本也稍微短一些。这是很典型的。事实上,HQL 查询通常比它们编译为的 SQL 更紧凑。

但有一个巨大的区别:在 HQL 中,Book 指的是用 Java 编写的实体类,而 book.title 指的是该类的字段。我们不被允许在 HQL 或 JPQL 中直接引用数据库表和列。

在本章中,我们将通过快速概述基本语句类型来展示 HQL 与 SQL 的相似之处。您会无聊地发现它们正是您所期望的:selectinsertupdatedelete

这是参考指南。我们不会解释诸如三值逻辑、联接、聚合、选择或投影之类的基本概念,因为这些信息在其他地方很容易获得,而且无论如何,我们不可能在这里公正地处理这些主题。如果您对这些想法没有牢固的把握,那么是时候拿起一本有关 SQL 或关系模型的书了。

但首先,我们需要提一下与 SQL 有点不同的一件事。HQL 有一种稍微复杂的方式来处理大小写敏感问题。

1.2. 词法结构

从词法上讲,JPQL 与 SQL 非常相似,所以在本节中,我们将仅限于提到它不同的那些地方。

1.2.1. 标识符与大小写敏感性

标识符是用于引用实体、Java 类的属性、标识变量 或函数的名称。

比如,Booktitleauthorupper 都是标识符,但它们指代不同类型的事物。在 HQL 和 JPQL 中,标识符的大小写敏感性取决于标识符所指的事物的类型。

大小写敏感性的规则是

  • 关键字和函数名称不区分大小写,但是

  • 标识变量名称、Java 类名称和 Java 类属性的名称区分大小写。

我们对这种不一致性表示歉意。事后看来,将整个语言定义为大小写敏感可能会更好。

顺便说一句,在 HQL 中使用小写关键字是标准做法。

使用大写关键字表示了一种对 1970 年代文化可爱但又不健康的依恋。

只是为了重申这些规则

selectSeLeCTsELEctSELECT

全部相同,select 是一个关键字

upper(name)UPPER(name)

相同,upper 是一个函数名称

from BackPackfrom Backpack

不同,指代不同的 Java 类

person.nickNameperson.nickname

不同,因为路径表达式元素 nickName 指代在 Java 中定义的实体的属性

person.nickNamePerson.nickNamePERSON.nickName

全部不同,因为路径表达式的第一个元素是一个标识变量

JPQL 规范将标识变量定义为不区分 大小写 的变量。因此,在严格的 JPA 兼容模式中,Hibernate 会将 person.nickNamePerson.nickNamePERSON.nickName 视为 相同 的变量。

带引号的标识符 用反引号编写。加引号可让你将关键词用作标识符。

select thing.interval.`from` from Thing thing

实际上,在大多数情况下,HQL 关键词是“软”的,不需要加引号。解析器通常能够区分保留字是用作关键词还是标识符。

1.2.2. 注释

HQL 中的注释看起来像 Java 中的多行注释。它们以 /**/ 为界。

不允许使用 SQL 样式的 -- 注释或 Java 样式的 // 行尾注释。

在 HQL 中看到注释的情况非常罕见,但使用 Java 文本块后可能这种情况会变得更加普遍。

1.2.3. 参数

JPQL 中有两种类型的参数,而 HQL 出于历史原因支持第三种类型

参数类型 示例 在 Java 中的用法

命名参数

:name:title:id

query.setParameter("name", name)

序号参数

?1, ?2, ?3

query.setParameter(1, name)

JDBC 风格的参数 💀

?

query.setParameter(1, name)

? 形式的 JDBC 风格参数类似于序号参数,其中索引是从查询文本中的位置推断出来的。JDBC 风格的参数已被弃用。

使用参数将用户输入传递给数据库非常重要。通过将 HQL 片段与用户输入连接来构建查询是非常危险的,因为它可能导致在数据库服务器上执行任意代码。

1.2.4. 文字

某些字面值语法的也与 ANSI SQL 中的标准语法有所偏离,特别是在日期/时间字面值领域,但稍后我们将在 字面值 中讨论这一切。

1.3. 语法

我们将边讲解边描述语言的语法,有时会使用类似 ANTLR-BNF 的形式显示语法片段。(有时我们会为了可读性简化这些片段,因此请不要将它们视为规范。)

有关 HQL 的完整规范语法可以在 github 项目 中找到。

可在 JPA 规范的第 4 章中找到 JPQL 语法。

1.4. 类型系统

JPA 没有明确指定的类型系统,但从字里行间可以看出,可以判别以下类型

  • 实体类型、

  • 数值、

  • 字符串、

  • 日期/时间、

  • 布尔值,以及

  • 枚举类型。

从某种意义上说,这种粒度较粗的类型系统对规范的实施者来说是一种不足够的约束,或者从不同的角度来看,它留给我们很大的灵活性。

HQL 对此类型系统的解释是对语言中的每一个表达式分配一个 Java 类型。因此,数字表达式的类型如 LongFloatBigInteger,日期/时间表达式的类型如 LocalDateLocalDateTimeInstant,布尔表达式永远是 Boolean 类型。

进一步来说,像 local datetime - document.created 这样的表达式被分配 Java 类型 java.time.Duration,这个类型在 JPA 规范中没有任何地方出现。

由于语言必须在 SQL 数据库中执行,因此每种类型都要容纳空值。

1.4.1. Null 值和三元逻辑

SQL 中的 null 与 Java 中的空值的行为非常不同。

  • 在 Java 中,如果 number 为空,则像 number + 1 这样的表达式在执行中会产生异常。

  • 但是在 SQL 中,以及也在 HQL 和 JPQL 中,这样的表达式求值为 null

几乎总是这样,对空值应用的操作会产生另一个空值。此规则适用于函数应用,适用于 *|| 等运算符,适用于 <= 等比较运算符,甚至适用于 andnot 等逻辑运算。

此规则的例外是 is null 运算符以及专门用于 处理空值coalesce()ifnull() 函数。

此规则是 SQL 中著名的(且有争议的)三元逻辑 的根源。像 firstName='Gavin' and team='Hibernate' 这样的逻辑表达式不限于值 truefalse。它也可能是 null

从原则上讲,这可能导致一些极其不直观的结果:我们不能使用排中律来推理 SQL 中的逻辑表达式!但在实践中,我们从未遇到过这种情况给我们造成问题。

你现在可能知道,当逻辑谓词作为 限制 出现时,谓词求值为 null 的行将从结果集中排除。也就是说,至少在此上下文中,逻辑空值解释为“实际上为 false”。

1.5. 语句类型

HQL 包含四种不同类型的语句

  • select 查询、

  • update 语句、

  • delete 语句、以及

  • insert …​ valuesinsert …​ select 语句。

insertupdatedelete 语句通常被称为变更查询。通过有状态会话执行变更查询时,我们需要小心谨慎。

在语句执行时,updatedelete 语句的效果不会反映在持久性上下文中,也不会反映在内存中保存的实体对象的状态中。

在执行了更新语句或删除语句后,维护内存中保留状态与数据库同步的责任在于客户端程序。

接下来让我们考虑每种类型的更改查询,首先从最常用的类型开始。

1.5.1. 更新语句

update 语句的巴科斯范式 (Backus-Naur Form,简写为 BNF) 非常简单。

updateStatement
    : "UPDATE" "VERSIONED"? targetEntity setClause whereClause?

targetEntity
	: entityName variable?

setClause
	: "SET" assignment ("," assignment)*

assignment
    : simplePath "=" expression

set 子句有一系列针对给定实体属性的分配。

例如

update Person set nickName = 'Nacho' where name = 'Ignacio'

更新语句是 polymorphic 的,并影响给定实体类中的映射子类。因此,单个 HQL update 语句可能会导致针对数据库执行多条 SQL 更新语句。

必须使用 Query.executeUpdate() 执行 update 语句。

// JPA API
int updatedEntities = entityManager.createQuery(
        "update Person p set p.name = :newName where p.name = :oldName")
            .setParameter("oldName", oldName)
            .setParameter("newName", newName)
            .executeUpdate();
// Hibernate native API
int updatedEntities = session.createMutationQuery(
        "update Person set name = :newName where name = :oldName")
            .setParameter("oldName", oldName)
            .setParameter("newName", newName)
            .executeUpdate();

executeUpdate() 返回的整型值表示受操作影响的实体实例数量。

JOINED 继承层次结构中,要存储单个实体实例需要多条行。在此情况下,Hibernate 返回的更新数量可能并不完全等同于数据库中受影响的行数。

默认情况下,update 语句不会影响受影响实体的 @Version 属性映射的列。

添加关键字 versioned,编写 update versioned,指定 Hibernate 应当增加版本号或更新最后修改时间戳。

update versioned Book set title = :newTitle where ssn = :ssn

update 语句可能不会直接联接其他实体,但可以

  • 有一个 隐式联接,或者

  • 在其 set 子句或 where 子句中包含子查询,而子查询可能包含联接。

1.5.2. 删除语句

delete 语句的 BNF 更简单

deleteStatement
    : "DELETE" "FROM"? targetEntity whereClause?

例如

delete Author author where is empty author.books

与 SQL 中一样,是否存在 from 关键字完全不会影响 delete 语句的语义。

与更新语句一样,删除语句是 polymorphic 的,并影响给定实体类中的映射子类。因此,单个 HQL delete 语句可能会导致针对数据库执行多条 SQL 删除语句。

通过调用 Query.executeUpdate() 执行 delete 语句。

executeUpdate() 返回的整型值表示受操作影响的实体实例数量。

delete 语句可能不会直接联接其他实体,但可以

  • 有一个 隐式联接,或者

  • 在其 where 子句中包含子查询,而子查询可能包含联接。

1.5.3. 插入语句

有两种 insert 语句

  • insert …​ values,其中要插入的属性值直接作为元组给出,以及

  • insert …​ select,其中插入的属性值源自子查询。

第一种形式在数据库中插入一行或多行(如果在 values 子句中提供了多个元组)。第二种形式可以插入许多新行,或什么都不插入。

第一种 insert 语句没有那么实用。通常最好是只使用 persist()

但你可以考虑使用它来设定测试数据。

insert 语句不是 JPQL 的一部分。

insert 语句的 BNF 如下所示:

insertStatement
    : "INSERT" "INTO"? targetEntity targetFields
      (queryExpression | valuesList)
      conflictClause?

targetEntity
	: entityName variable?

targetFields
	: "(" simplePath ("," simplePath)* ")"

valuesList
	: "VALUES" values ("," values)*

values
	: "(" expression ("," expression)* ")"

例如

insert Person (id, name)
    values (100L, 'Jane Doe'), (200L, 'John Roe')
insert into Author (id, name, bio)
    select id, name, name || ' is a newcomer for ' || str(year(local date))
    from Person
    where id = :pid

与 SQL 中一样,into 关键字的存在与否对 insert 语句的语义没有影响。

从这些示例中,我们可能会注意到 insert 语句在一点上与 updatedelete 语句不同。

insert 语句本质上不是多态的!其目标字段列表为固定长度,而实体类的每个子类可以声明其他字段。如果实体涉及到映射继承层次结构,则只可将目标字段列表中声明的属性直接指定给命名的实体及其超类。不得指定子类声明的属性。

insert …​ select 语句中的 queryExpression 可以是任何有效的 select 查询,但例外是 select 列表中的值类型必须与目标字段类型匹配。

将在查询编译期间检查这一点,而不是将类型检查委托给数据库。当两个 Java 类型映射到相同的数据库类型时,这可能会导致问题。例如,类型为 LocalDateTime 的属性和类型为 Timestamp 的属性或类型都映射到 SQL 类型 timestamp,但查询编译器不会将它们视为可分配的。

有两种方法可将值指定给 @Id 属性:

  • 在目标字段列表中明确指定 id 属性,以及分配给目标字段的值,或者

  • 省略它,在这种情况下,将使用生成的 value。

当然,第二个选项仅适用于采用数据库级 id 生成(序列或标识列/自动增量列)的实体。对于其 id 生成器在 Java 中实现的实体,也不可用,对于其 id 由应用程序分配的实体也不可用。

@Version 属性也有相同这两个选项。当未明确指定版本时,将使用新的实体实例版本。

on conflict 子句允许我们在数据库中已包含我们尝试插入的记录时,指定应采取什么操作。

conflictClause
	: ON CONFLICT conflictTarget? "DO" conflictAction

conflictTarget
	: ON CONSTRAINT identifier
	| "(" simplePath ("," simplePath)* ")"

conflictAction
	: "NOTHING"
	| "UPDATE" setClause whereClause?

请注意,只接受唯一约束名称的 on constraint 变量仅在特定数据库或仅当插入一行时适用。

insert Person (ssn, name, phone)
    values ('116-76-1234', 'Jane Doe', '404 888 4319')
    on conflict (ssn) do update
        set phone = excluded.phone

updatedelete 语句一样,必须通过调用 Query.executeUpdate() 执行 insert 语句。

现在是时候查看一些更加复杂的内容了。

1.5.4. 选择语句

Select 语句检索并分析数据。这正是我们真正想要了解的。

select 查询的完整 BNF 非常复杂,但现在无需理解它。我们在此展示它以备将来参考。

selectStatement
	: queryExpression

queryExpression
	: withClause? orderedQuery (setOperator orderedQuery)*

orderedQuery
	: (query | "(" queryExpression ")") queryOrder?

query
	: selectClause fromClause? whereClause? (groupByClause havingClause?)?
	| fromClause whereClause? (groupByClause havingClause?)? selectClause?
	| whereClause

queryOrder
	: orderByClause limitClause? offsetClause? fetchClause?

fromClause
	: "FROM" entityWithJoins ("," entityWithJoins)*

entityWithJoins
	: fromRoot (join | crossJoin | jpaCollectionJoin)*

fromRoot
	: entityName variable?
	| "LATERAL"? "(" subquery ")" variable?

join
	: joinType "JOIN" "FETCH"? joinTarget joinRestriction?

joinTarget
	: path variable?
	| "LATERAL"? "(" subquery ")" variable?

withClause
	: "WITH" cte ("," cte)*
	;

此处的复杂性主要由 set 运算符(unionintersectexcept)与排序交互作用引起。

我们将在以后的根实体和联接选择、投影和聚合中描述查询的各种子句,不过现在总结一下,一个查询可以有以下内容

子句 术语 用途

with

公用表表达式

声明要在后续查询中使用的命名子查询

fromjoin

根和联接

指定查询中涉及的实体,以及它们彼此之间如何关联

where

选择/限制

指定查询返回的数据的限制

group by

聚合/分组

控制聚合

having

选择/限制

在聚合应用限制

select

投影

指定投影(需要从查询中返回的内容)

unionintersectexcept

集合代数

这些是应用于多个子查询结果的集合运算符

order by

排序

指定结果应如何排序

limitoffsetfetch

限制

允许限制或分页结果

这些子句每个都是可选的!

例如,HQL 中最简单的查询根本没有 select 子句

from Book

但我们不一定推荐省略 select 列表。

HQL 不要求 select 子句,但 JPQL 要求

当然,前面的查询可以用 select 子句编写

select book from Book book

但是当没有明确的 select 子句时,选择列表由查询的结果类型隐含定义。

// result type Book, only the Book selected
List<Book> books =
        session.createQuery("from Book join authors", Book.class)
            .getResultList();
for (Book book: books) {
    ...
}
// result type Object[], both Book and Author selected
List<Object[]> booksWithAuthors =
       session.createQuery("from Book join authors", Book.class, Object[].class)
            .getResultList();
for (var bookWithAuthor: booksWithAuthors) {
    Book book = (Book) bookWithAuthor[0];
    Author author = (Author) bookWithAuthor[1];
    ...
}

对于复杂的查询,最好明确指定 select 列表。

另一种“最简单”的查询只有一个 select 列表

select local datetime

这会导致一个 SQL from dual 查询(或等效的查询)。

仔细查看上面给出的 BNF,您可能会注意到,select 列表可能出现在查询的开头,或结尾附近,就在 order by 之前。

当然,标准的 SQL 和 JPQL 要求 select 列表出现在开头。但是将其放在最后更自然

from Book book select book.title, book.isbn

这种形式的查询具有更好的可读性,因为别名是在使用之前声明的,正如 God 和自然预期的那样。

当然,查询总是多态的。事实上,一个看似相当无害的 HQL 查询很容易转换为包含许多联接和并集的 SQL 语句。

我们对此需要非常谨慎,但实际上它通常是一件好事。HQL让我们能够非常轻松地一次性获取数据库所需的所有数据,而且这对实现数据访问代码的高性能至关重要。一般来说,如果一次数据库服务器往返行程能获取我们所需的所有数据,那要比一次 SQL 查询获取比所需数据略多的数据要好得多,而不是往返多次。

如果没有显式 select 定语,有时还可以进一步简化。

select 查询的结果类型是实体类型,而且我们通过将实体类传递给 createQuery()createSelectionQuery() 显式指定类型时,我们有时可以省略 from 定语,例如

// explicit result type Book, so 'from Book' is inferred
List<Book> books =
        session.createQuery("where title like :title", Book.class)
            .setParameter("title", title)
            .getResultList();

1.6. 在 Java 中表示结果集

在 Java 中使用数据最不舒服的一点在于无法很好地表示表格。为处理数据而设计的语言(例如 R)始终会提供某种内置表格或“数据框架”类型。当然,Java 的类型系统在这里起到了阻碍作用。这个问题在动态类型语言中更容易解决。Java 的一个基本问题是它没有元组类型。

Hibernate 中的查询会返回表格。当然,通常一列会存储整个实体对象,但我们并不受限于返回单个实体,而且我们经常编写返回每个结果中多个实体或返回非实体内容的查询。

因此,我们面临这样一个问题,即表示此类结果集,而且我们很遗憾地说,没有完全通用的、完全令人满意的解决方案。

让我们从简单的情况开始。

1.6.1. 具有单个投影项的查询

如果 select 列表中只有一个投影项,那么不用担心,那个就是每个查询结果的类型。

List<String> results =
        entityManager.createQuery("select title from Book", String.class)
            .getResultList();

我们真的不需要忙于尝试表示“长度为 1 的元组”。我们甚至不知道如何称谓它们。

只要在查询的 select 列表中有多个项,就会出现问题。

1.6.2. 具有多个投影项的查询

如果选择列表中有多个表达式,那么默认情况下,且为了符合 JPA,每个查询结果都会打包为类型为 Object[] 的数组。

List<Object[]> results =
        entityManager.createQuery("select title, left(book.text, 200) from Book",
                                  Object[].class)
            .getResultList();
for (var result : results) {
	String title = (String) result[0];
	String preamble = (String) result[1];
}

这是可以忍受的,但我们来探讨一些其他选项。

JPA 让我们能够指定将每个查询结果打包为 jakarta.persistence.Tuple 实例。我们只需将类 Tuple 传递给 createQuery() 即可。

List<Tuple> tuples =
        entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book",
                                  Tuple.class)
            .getResultList();
for (Tuple tuple : tuples) {
    String title = tuple.get("title", String.class);
    String preamble = tuple.get("preamble", String.class);
}

Tuple 元素的名称由选择列表中投影项指定的别名确定。如果没有指定别名,则可以通过列表中的位置访问元素,其中第一项被指定为位置零。

作为 JPA 的延伸,并且以类似的方式,Hibernate 允许我们传递 MapList,并将每个结果打包为地图或列表

var results =
        entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book",
                                  Map.class)
            .getResultList();
for (var map : results) {
    String title = (String) map.get("title");
    String preamble = (String) map.get("preamble");
}
var results =
        entityManager.createQuery("select title, left(book.text, 200) from Book",
                                  List.class)
            .getResultList();
for (var list : results) {
    String title = (String) list.get(0);
    String preamble = (String) list.get(1);
}

遗憾的是,Object[]ListMapTuple 类型都不允许我们在不进行类型转换的情况下访问结果元组中的单个项目。当然,当我们将类对象传递给 get() 时,Tuple 会为我们进行类型转换,但这在逻辑上是相同的。幸运的是,还有一种选择,我们即将看到这一点。

事实上,Tuple 实际上是为了服务于标准查询 API 而存在的,在这种情况下,它确实支持对查询结果进行真正类型安全的访问。

Hibernate 6 允许我们向 createQuery() 传递一个具有适当构造函数的任意类类型,并将使用它来打包查询结果。它与 record 类型非常合适。

record BookSummary(String title, String summary) {}

List<BookSummary> results =
        entityManager.createQuery("select title, left(book.text, 200) from Book",
                                  BookSummary.class)
            .getResultList();
for (var result : results) {
    String title = result.title();
    String preamble = result.summary();
}

重要的是,BookSummary 的构造函数具有的参数应与 select 列表中的项目完全匹配。

此类无需以任何方式映射或注释。

即使该类一个实体类,生成实例也不是托管实体,并且与会话相关联。

我们必须注意,此操作仍然不是类型安全的。事实上,我们刚刚将类型转换推入到对 createQuery() 的调用中。但至少我们不必明确编写它们。

1.6.3. 实例化

在 JPQL 和旧版本的 Hibernate 中,此功能需要更多仪式。

结果类型 旧语法 简化语法 JPA 标准

地图

select new map(x, y)

select x, y

✖/✖

列表

select new list(x, y)

select x, y

✖/✖

任意类 Record

select new Record(x, y)

select x, y

✔/✖

例如,JPA 标准 select new 构造将查询结果打包到用户编写的 Java 类中,而不是数组中。

record BookSummary(String title, String summary) {}

List<BookSummary> results =
        entityManager.createQuery("select new BookSummary(title, left(book.text, 200)) from Book",
                                  BookSummary.class)
            .getResultList();
for (var result : results) {
    String title = result.title();
    String preamble = result.summary();
}

稍微简化一下,投影项目的 BNF 为

selection
    : (expression | instantiation) alias?

instantiation
    : "NEW" instantiationTarget "(" selection ("," selection)* ")"

alias
    : "AS"? identifier

instantiation 中的 selection 列表本质上是一个嵌套投影列表。

2. 表达式

现在我们切换档位,并开始从底层描述这门语言。一种编程语言的最底层是它针对字面值的语法。

2.1. 文字

此语言中最重要字面值是 null。它可以分配给任何其他类型。

2.1.1. 布尔文字

布尔字面值是(不区分大小写的)关键字 truefalse

2.1.2. 字符串文字

字符串文字用单引号引起来。

select 'hello world'

要在字符串文字中转义单引号,请使用双重单引号:''

from Book where title like 'Ender''s'

或者,Java 风格的双引号字符串也是允许的,并具有通常的 Java 字符转义语法。

select "hello\tworld"

此选项不常用。

2.1.3. 数值文字

数字文字有几种不同的形式

类型 类型 示例

整型文字

LongIntegerBigInteger

13_000_000L2BI

十进制字面量

DoubleFloatBigDecimal

1.0123.456F3.14159265BD

十六进制字面量

LongInteger

0X1A2B0x1a2b

科学计数法

DoubleFloatBigDecimal

1e-66.674E-11F

例如

from Book where price < 100.0
select author, count(book)
from Author as author
    join author.books as book
group by author
having count(book) > 10

可以使用 Java 样式后缀指定数字字面量的类型

后缀 类型 Java 类型

Ll

长整型

long

Dd

双精度

double

Ff

单精度

float

BIbi

大整数

BigInteger

BDbd

精确十进制数

BigDecimal

通常不需要显式指定精度。

在带有指数的字面量中,E 不区分大小写。类似地,Java 样式后缀也不区分大小写。

2.1.4. 日期和时间文字

根据 JPQL 规范,可以使用 JDBC 转义语法指定日期和时间字面量。由于此语法看起来不太美观,因此 HQL 提供了不只一种的替代方案。

日期/时间类型 建议的 Java 类型 JDBC 转义语法 💀 带大括号的字面量语法 带显式类型的字面量语法

日期

LocalDate

{d 'yyyy-mm-dd'}

{yyyy-mm-dd}

日期 yyyy-mm-dd

时间

LocalTime

{t 'hh:mm'}

{hh:mm}

时间 hh:mm

带秒的时间

LocalTime

{t 'hh:mm:ss'}

{hh:mm:ss}

时间 hh:mm:ss

日期时间

LocalDateTime

{ts 'yyyy-mm-ddThh:mm:ss'}

{yyyy-mm-dd hh:mm:ss}

日期时间 yyyy-mm-dd hh:mm:ss

带毫秒的日期时间

LocalDateTime

{ts 'yyyy-mm-ddThh:mm:ss.millis'}

{yyyy-mm-dd hh:mm:ss.millis}

日期时间 yyyy-mm-dd hh:mm:ss.millis

带偏移量的日期时间

OffsetDateTime

{ts 'yyyy-mm-ddThh:mm:ss+hh:mm'}

{yyyy-mm-dd hh:mm:ss +hh:mm}

日期时间 yyyy-mm-dd hh:mm:ss +hh:mm

带时区的日期时间

OffsetDateTime

{ts 'yyyy-mm-ddThh:mm:ss GMT'}

{yyyy-mm-dd hh:mm:ss GMT}

日期时间 yyyy-mm-dd hh:mm:ss GMT

还提供了引用当前日期和时间的字面量。同样也有一定灵活性。

日期/时间类型 Java 类型 带下划线的语法 带空格的语法

日期

java.time.LocalDate

local_date

本地日期

时间

java.time.LocalTime

local_time

本地时间

日期时间

java.time.LocalDateTime

local_datetime

本地日期时间

偏移日期时间

java.time.OffsetDateTime

offset_datetime

偏移日期时间

瞬时

java.time.Instant

瞬时

瞬时

日期

java.sql.Date 💀

current_date

当前日期

时间

java.sql.Time 💀

current_time

当前时间

日期时间

java.sql.Timestamp 💀

current_timestamp

当前时间戳

其中,只有 local datelocal timelocal datetimecurrent_datecurrent_timecurrent_timestamp 由 JPQL 规范定义。

强烈建议不要使用 java.sql 包中的日期和时间类型!在新代码中应始终使用 java.time 类型。

2.1.5. 持续时间文字

HQL 中有两种持续时间

  • 年日持续时间,即两个日期之间的间隔长度和

  • 周纳秒持续时间,即两个日期时间之间的间隔长度。

由于概念原因,这两种持续时间不能清晰地组成。

按形式 n unit 表示文字持续时间表达式,例如,1 day10 year100 nanosecond

select start, end, start - 30 minute from Event

单位可以是:dayweekmonthquarteryearsecondminutehournanosecond

HQL 持续时间被认为映射到 Java java.time.Duration,但语义上或许更类似于 ANSI SQL INTERVAL 类型。

2.1.6. 二进制字符串文字

HQL 还为二进制字符串提供了格式选项

  • 带大括弧的语法 {0xDE, 0xAD, 0xBE, 0xEF},一组 Java 风格十六进制字节文字,或

  • 带引号的语法 X’DEADBEEF'x’deadbeef',类似于 SQL。

2.1.7. 枚举文字

Java 枚举类型的文字值可以写入,而不必指定枚举类名称

from Book where status <> OUT_OF_PRINT
from Book where type in (BOOK, MAGAZINE)
update Book set status = OUT_OF_PRINT

在以上示例中,枚举类从比较左侧、分配运算符或 in 谓词中的表达式类型推断。

from Book
order by
    case type
    when BOOK then 1
    when MAGAZINE then 2
    when JOURNAL then 3
    else 4
    end

在本示例中,枚举类从 case 表达式的类型推断。

2.1.8. Java 常量

HQL 允许任何 Java static 常量在 HQL 中使用,但必须按其完全限定名称引用

select java.lang.Math.PI

2.1.9. 字面实体名称

实体名称也可作为文字值出现。它们不需要限定。

from Payment as payment
where type(payment) = CreditCardPayment

请参阅 类型和类型转换

2.2. 标识变量和路径表达式

路径表达式是下面任一项

  • 识别变量 的引用,或

  • 一个以对识别变量的引用作为开头的复合路径,后面是实体属性引用组成的用句点分隔的列表。

作为对 JPA 规范的扩展,HQL 与 SQL 一样,允许复合路径表达式中的路径开头的识别变量缺失。也就是说,不必写入 var.foo.bar,可以合法地仅写入 foo.bar。但这仅在可以从复合路径的第一个元素(foo)中明确推断出识别变量时才允许。该查询必须正好有一个识别变量 var,它的路径 var.foo 指向实体属性。请注意,即使路径只有一个元素,但我们仍继续称其为“复合”路径。

当只有一个根实体且没有连接时,这种方式可以很好地简化查询。但是,当查询有多个识别变量时,这种方式会让查询难以理解。

如果复合路径中的元素引用关联,则路径表达式会产生隐式连接

select book.publisher.name from Book book

复合路径中引用一对多或一对一关联的元素可以应用treat函数。

select treat(order.payment as CreditCardPayment).creditCardNumber from Order order

如果复合路径中的元素引用集合或多元关联,则必须对它应用这些特殊函数之一。

select element(book.authors).name from Book book

不能对路径表达式的非终端元素应用任何其他函数。

或者,如果复合路径的元素引用列表或映射,则可以对它应用索引运算符。

select book.editions[0].date from Book book

不得对路径表达式的非终端元素应用任何其他运算符。

2.3. 运算符表达式

HQL 具有用于处理字符串、数值和日期/时间类型的运算符。

运算符优先级由下表指定,从最高优先级到最低优先级。

优先级类别 类型 运算符

分组和元组实例化

( …​ )(x, y, z)

案例列表

case …​ end

成员引用

二元中缀

a.b

函数应用

后缀

f(x,y)

索引

后缀

a[i]

一元数值

一元前缀

+, -

持续时间转换

一元后缀

by day 和类似项

二元乘法

二元中缀

*, /, %

二元加法

二元中缀

+, -

连接

二元中缀

||

空值、空集、真值

一元后缀

is nullis emptyis trueis false

包含

二元中缀

innot in

介于之间

三元中缀

betweennot between

模式匹配

二元中缀

likeilikenot likenot ilike

比较运算符

二元中缀

=, <>, <, >, <=, >=

空值安全比较

二元中缀

is distinct fromis not distinct from

存在性

一元前缀

exists

成员资格

二元中缀

member ofnot member of

逻辑否定

一元前缀

not

逻辑合取

二元中缀

and

逻辑析取

二元中缀

or

2.3.1. 字符串连接

HQL 定义了两种将字符串连接起来的方法

  • SQL 风格的连接运算符 ||,以及

  • JPQL 标准的 concat() 函数。

有关 concat() 函数的详细信息,请参阅以下内容

select book.title || ' by ' || listagg(author.name, ' & ')
from Book as book
    join book.authors as author
group by book

更多有关字符串的操作在函数的下方进行定义。

2.3.2. 数字算术运算

基本 SQL 算术运算符 +-*/ 由余数运算符 % 连接起来。

select (1.0 + :taxRate) * sum(item.book.price * item.quantity)
from Order as ord
    join ord.items as item
where ord.id = :oid

当二元数字运算符的两个操作数类型相同 时,整个表达式的结果类型与操作数相同。

默认情况下,整数除法的语义取决于数据库

  • 在大多数数据库中,将一个整数除以另一个整数的结果是一个整数,就像在 Java 中一样。因此,3/2 的结果为 1

  • 但在某些数据库中,包括 Oracle、MySQL 和 MariaDB,整数除法可能产生非整数。因此,在这些数据库上,3/2 的结果为 1.5

可以使用配置属性 hibernate.query.hql.portable_integer_division 更改此默认行为。将此属性设置为 true 会指示 Hibernate 在非本地语义的平台上生成仿真 Java 样式整数除法的 SQL(即 3/2 = 1)。

当操作数类型不同时,一个操作数将隐式转换为更宽的类型,宽度按降序由以下列表给出

  • Double(最宽)

  • Float

  • BigDecimal

  • BigInteger

  • Long

  • Integer

  • Short

  • Byte

更多数字操作定义于下方 函数 中。

2.3.3. 日期时间算术运算

涉及日期、日期时间和持续时间的算术运算非常微妙。需要考虑的问题包括

  • 有两种类型的持续时间:年-天持续时间和周-纳秒持续时间。前者是日期之间的差异;后者是日期时间之间的差异。

  • 我们可以减去日期和日期时间,但我们不能加上它们。

  • Java 样式的持续时间具有过高的精度,因此为了将其用于任何有用的目的,我们必须以某种方式将其截断为粒度更粗的东西。

在此列出基本操作。

运算符 表达式类型 示例 结果类型

-

两个日期之间的差异

your.birthday - local date

年-天持续时间

-

两个日期时间之间的差异

local datetime - record.lastUpdated

周-纳秒持续时间

-

一个日期和一个年-天持续时间的差异

local date - 1 day

日期

-

一个日期时间和一个周-纳秒持续时间的差异

record.lastUpdated - 1 minute

日期时间

-

两个持续时间之间的差异

1 week - 1 day

持续时间

+

一个日期和一个年-天持续时间的和

local date + 1 week

日期

+

一个日期时间和一个周-纳秒持续时间的和

record.lastUpdated + 1 second

日期时间

+

两个持续时间的和

1 day + 4 hour

持续时间

*

一个整数和一个持续时间的乘积

billing.cycles * 30 day

持续时间

按单位

将持续时间转换为整数

(1 year) by day

整数

by unit 运算符将持续时间转换为整数,例如:(local date - your.birthday) by day 会计算您还需要等待的天数。

函数 extract(unit from …​) 从日期、时间或 datetime 类型中提取一个字段,例如,extract(year from your.birthday) 会返回您出生的年份,并丢弃关于您生日的重要信息。

请仔细注意这两个操作之间的差异:byextract() 都计算为整数,但它们的使用情况不同。

“函数” 中定义了其他日期时间操作,包括实用的 format() 函数。

2.4. Case 表达式

就像标准 SQL 中一样,CASE 表达式有两种形式:

  • 简单的 CASE 表达式,和

  • 所谓的搜索 CASE 表达式。

CASE 表达式很冗长。使用“处理 null 值的函数”中描述的 coalesce()nullif()ifnull() 函数通常更简单,如下所示:处理 null 值的函数

简单的 CASE 表达式

简单形式的语法由以下定义:

"CASE" expression ("WHEN" expression "THEN" expression)+ ("ELSE" expression)? "END"

例如

select
    case author.nomDePlume
        when '' then person.name
        else author.nomDePlume end
from Author as author
    join author.person as person
搜索 CASE 表达式

搜索形式具有以下语法:

"CASE" ("WHEN" predicate "THEN" expression)+ ("ELSE" expression)? "END"

例如

select
    case
        when author.nomDePlume is null then person.name
        else author.nomDePlume end
from Author as author
    join author.person as person

case 表达式可能包含复杂的表达式,包括运算符表达式。

2.5. 元组

元组实例是一个表达式,如 (1, 'hello'),可用于“矢量化”比较表达式。

from Person where (firstName, lastName) = ('Ludwig', 'Boltzmann')
from Event where (year, day) > (year(local date), day(local date))

即使基础 SQL 方言不支持所谓的“行值”构造函数,也可以使用此语法。

元组值可能会与嵌入式字段进行比较:

from Person
where address = ('1600 Pennsylvania Avenue, NW', 'Washington', 'DC', 20500, 'USA')

2.6. 函数

HQL 和 JPQL 都会定义一些标准函数,并使其可以在数据库间移植。

希望在 Jakarta Persistence 提供程序之间保持可移植性的程序原则上应仅限于使用规范认可的函数。遗憾的是,这样的函数并不多。

在某些情况下,这些函数的语法乍看起来有些奇怪,例如 cast(number as String),或 extract(year from date),甚至 trim(leading '.' from string)。此语法受标准 ANSI SQL 启发,我们承诺您会习惯它的。

HQL 会抽象出实际的数据库本机 SQL 函数,方便编写在数据库间可移植的查询。

对于某些函数以及依赖于数据库的函数,HQL 函数调用会转换为相当复杂的 SQL 表达式!

此外,还有多种方法可以使用 Hibernate 不认识的数据库函数。

2.6.1. 类型和类型转换

以下特殊函数可用于查找或缩小表达式类型:

特殊函数 用途 签名 JPA 标准

type()

(具体)实体或可嵌入类型

type(e)

treat()

缩小实体或可嵌入类型

treat(e as Entity)

cast()

缩小基本类型

cast(x as Type)

str()

转换为字符串

str(x)

我们来看看这些函数的作用。

计算实体类型

应用于标识变量或实体值或可嵌入值路径表达式的函数 type() 计算为具体类型,即被引用的实体或可嵌入的 Java Class。这在处理实体继承层次结构时特别有用。

select payment
from Payment as payment
where type(payment) = CreditCardPayment
缩小实体类型

函数treat()可用于缩小标识变量的类型。在处理实体或嵌入式继承层次结构时,此功能非常有用。

select payment
from Payment as payment
where length(treat(payment as CreditCardPayment).cardNumber)
        between 16 and 20

表达式treat(p as CreditCardPayment)的类型为缩小的类型CreditCardPayment,而不是p 的已声明类型Payment。这允许引用子类型CreditCardPayment声明的属性cardNumber

  • 第一个参数通常是标识变量。

  • 第二个参数是作为不合格实体名称给出的目标类型。

treat()函数甚至可能出现在联接中。

常规类型转换

函数cast()具有类似的语法,但用于缩小基本类型。

  • 其第一个参数通常是实体的属性,或涉及实体属性的更复杂的表达式。

  • 其第二个参数是作为不合格 Java 类名称给出的目标类型:StringLongIntegerDoubleFloatCharacterByteBigIntegerBigDecimalLocalDateLocalTimeLocalDateTime 等等。

select cast(id as String) from Order
转换为字符串

函数str(x)cast(x as String)的同义词。

select str(id) from Order

2.6.2. 用于处理 null 值的函数

以下函数可轻松处理 Null 值

函数 用途 签名 JPA 标准

coalesce()

第一个非 Null 参数

coalesce(x, y, z)

ifnull()

如果第一个为 Null 则为第二个参数

ifnull(x,y)

nullif()

如果参数相等,则为null

nullif(x,y)

处理 Null 值

coalesce()函数是一种缩写的case表达式,用于返回第一个非 Null 操作数。

select coalesce(author.nomDePlume, person.name)
from Author as author
    join author.person as person
处理 Null 值

在只有两个参数的情况下,HQL 允许ifnull()作为coalesce()的同义词。

select ifnull(author.nomDePlume, person.name)
from Author as author
    join author.person as person
生成 Null 值

另一方面,如果nullif()操作数相等,则计算结果为 Null,否则计算结果为第一个参数。

select ifnull(nullif(author.nomDePlume, person.name), 'Real name')
from Author as author
    join author.person as person

2.6.3. 用于处理日期和时间的函数

有一些非常重要的函数可用于处理日期和时间。

特殊函数 用途 签名 JPA 标准

extract()

提取日期时间字段

extract(field from x)

格式化()

将日期时间格式化为字符串

format(datetime as pattern)

trunc()truncate()

日期时间截断

truncate(datetime, field)

提取日期和时间字段

特殊函数extract()获取日期、时间或日期时间的单个字段。

  • 其第一个参数是计算为日期、时间或日期时间的表达式。

  • 其第二个参数是日期/时间字段类型

识别出的字段类型如下所示。

字段 类型 范围 备注 JPA 标准

day

Integer

1-31

日历中一月的哪一天

month

Integer

1-12

year

Integer

week

Integer

1-53

ISO-8601 周数(不同于week of year

quarter

Integer

1-4

季度定义为 3 个月

hour

Integer

0-23

标准 24 小时制时间

minute

Integer

0-59

second

Float

0-59

包括小数秒

nanosecond

Long

数据库之间粒度不一致

星期

Integer

1-7

每月日期

Integer

1-31

day 的同义词

年度日期

Integer

1-365

每月星期数

Integer

1-5

每年星期数

Integer

1-53

纪元

Long

1970 年 1 月 1 日以来的秒数

日期

LocalDate

日期时间的一部分日期

时间

LocalTime

日期时间的一部分时间

偏移量

ZoneOffset

时区偏移量

小时偏移量

Integer

小时偏移量(小时)

分钟偏移量

Integer

0-59

分钟偏移量(分钟)

有关字段类型的完整列表,请参阅 TemporalUnit 的 Java 文档。

from Order where extract(date from created) = local date
select extract(year from created), extract(month from created) from Order

以下函数是 extract() 的缩写

函数 使用 extract() 的长格式 JPA 标准

year(x)

extract(year from x)

month(x)

extract(month from x)

day(x)

extract(day from x)

hour(x)

extract(hour from x)

minute(x)

extract(minute from x)

second(x)

extract(second from x)

这些缩写并非 JPQL 标准的一部分,但它们简洁明了。
select year(created), month(created) from Order
日期和时间格式化

format() 函数根据模式对日期、时间或日期时间进行格式化。

  • 其第一个参数是计算为日期、时间或日期时间的表达式。

  • 它的第二个参数是格式化模式,以字符串形式给出。

必须使用 Java 的 java.time.format.DateTimeFormatter 定义的模式语言子集编写此模式。

select format(local datetime as 'yyyy-MM-dd HH:mm:ss')

有关 format() 模式元素的完整列表,请参阅 Dialect.appendDatetimeFormat 的 Java 文档。

截断日期或时间类型

truncate() 函数将日期、时间或日期时间精度截断至由字段类型指定的临时单位。

  • 其第一个参数是计算为日期、时间或日期时间的表达式。

  • 它的第二个参数是日期/时间字段类型,指定截断值的精度。

支持的临时单位是:yearmonthdayhourminutesecond

select trunc(local datetime, hour)

截断日期、时间或日期时间值表示获取同类型的某个值,其中 field 中指明的所有临时单位均已删除。对于小时、分钟和秒,这意味着将其设置为 00。对于月份和日期,这意味着将其设置为 01

2.6.4. 用于处理字符串的函数

自然有相当多的函数用于处理字符串。

函数 用途 语法 JPA 标准/ANSI SQL 标准

upper()

字符串,将小写字符转换为大写

upper(str)

✔ / ✔

lower()

字符串,将大写字符转换为小写

lower(str)

✔ / ✔

length()

字符串长度

length(str)

✔ / ✖

concat()

连接字符串

concat(x, y, z)

✔ / ✖

locate()

字符串在字符串中位置

locate(patt, str),
locate(patt, str, start)

✔ / ✖

position()

类似于 locate()

position(patt in str)

✖ / ✔

substring()

字符串子串(JPQL 样式)

substring(str, 起始位置),
substring(str, 起始位置, 长度)

✔ / ✖

substring()

字符串的子串(ANSI SQL 风格)

substring(str from 起始位置),
substring(str from 起始位置 for 长度)

✖ / ✔

trim()

修剪字符串中的字符

trim(str),
trim(leading from str),
trim(trailing from str),或
trim(leading char from str)

✔ / ✔

overlay()

对于替换子串

overlay(str placing rep from 起始位置),
overlay(str placing rep from 起始位置 for 长度)

✖ / ✔

pad()

使用空格或指定字符填充字符串

pad(str with 长度),
pad(str with 长度 leading),
pad(str with 长度 trailing),或
pad(str with 长度 leading char)

✖ / ✖

left()

字符串的最左边的字符

left(str, 长度)

✖ / ✖

right()

字符串的最右边的字符

right(str, 长度)

✖ / ✖

replace()

替换字符串中模式的所有出现

replace(str, patt, rep)

✖ / ✖

repeat()

将字符串与自身串联多次

repeat(str, 次数)

✖ / ✖

collate()

选择校对规则

collate(p.name as collation)

✖ / ✖

让我们仔细看看其中一些。

与 Java 相反,字符串中字符的位置从 1 开始索引,而不是从 0!

串联字符串

JPQL 标准和 ANSI SQL 标准的 concat() 函数接受数量可变的参数,并通过串联它们来生成字符串。

select concat(book.title, ' by ', listagg(author.name, ' & '))
from Book as book
    join book.authors as author
group by book
查找子串

JPQL 函数 locate() 确定子串在另一个字符串中的位置。

  • 第一个参数是在第二个字符串中搜索的模式。

  • 第二个参数是要搜索的字符串。

  • 可选的第三个参数用于指定开始搜索的位置。

select locate('Hibernate', title) from Book

position() 函数具有类似的目的,但遵循 ANSI SQL 语法。

select position('Hibernate' in title) from Book
切分字符串

毫不奇怪,substring() 会返回给定字符串的子串。

  • 第二个参数指定子串中第一个字符的位置。

  • 可选的第三个参数指定子串的最大长度。

select substring(title, 1, position(' for Dummies' in title)) from Book         /* JPQL-style */

select substring(title from 1 for position(' for Dummies' in title)) from Book  /* ANSI SQL-style */
修剪字符串

trim() 函数遵循 ANSI SQL 的语法和语义。它可用于修剪 leading 字符、trailing 字符或两者。

select trim(title) from Book
select trim(trailing ' ' from text) from Book

它的 BNF 很怪

"TRIM" "(" (("LEADING" | "TRAILING" | "BOTH")? trimCharacter? "FROM")? expression ")" ;
填充字符串

pad() 函数的语法受 trim() 启发。

select concat(pad(b.title with 40 trailing '.'),
              pad(a.firstName with 10 leading),
              pad(a.lastName with 10 leading))
from Book as b
    join b.authors as a

它的 BNF 由以下给出

"PAD" "(" expression "WITH" expression ("LEADING" | "TRAILING") padCharacter? ")"
校对规则

collate() 函数选择要用于其字符串值参数的校对规则。校对规则对于使用 <> 进行 二进制比较 和在 order by 子句中很有用。

例如,collate(p.name as ucs_basic) 指定 SQL 标准对照 ucs_basic

对照在数据库之间不太可移植。
某些 PostgreSQL 对照名必须使用反引号引用,例如,collate(name as `zh_TW.UTF-8`)
@Collate 注释可以用来指定列的对照,这通常比使用 collate() 函数更方便。

2.6.5. 数值函数

当然,我们也有许多用来处理数值的函数。

函数 用途 签名 JPA 标准

abs()

一个数字的幅度

abs(x)

sign()

一个数字的符号

sign(x)

mod()

整数除法的余数

mod(n,d)

sqrt()

一个数字的平方根

sqrt(x)

exp()

指数函数

exp(x)

power()

指数运算

power(x,y)

ln()

自然对数

ln(x)

round()

数值舍入

round(number),
round(number, places)

trunc()truncate()

数值截断

truncate(number),
truncate(number, places)

floor()

向下取整函数

floor(x)

ceiling()

向上取整函数

ceiling(x)

log10()

以 10 为基的对数

log10(x)

log()

任意基的对数

log(b,x)

pi

π

pi

sin(), cos(), tan(), asin(), acos(), atan()

基本三角函数

sin(theta), cos(theta)

atan2()

双参数反正切(范围为 (-π,π]

atan2(y, x)

sinh(), cosh(), tanh()

双曲函数

sinh(x), cosh(x), tanh(x)

degrees()

将弧度转换成角度

degrees(x)

radians()

将角度转换成弧度

radians(x)

least()

返回给定参数中的最小值

least(x, y, z)

greatest()

返回给定参数中的最大值

greatest(x, y, z)

bitand(), bitor(), bitxor()

按位函数

bitand(x,y)

我们尚未在该列表中包含 聚合函数顺序集聚合函数窗口函数,因为它们的用途较为专门,并且附有额外的特殊语法。

2.6.6. 用于处理集合的函数

本节中描述的函数在处理 @ElementCollection 映射或涉及 @OrderColumn@MapKeyColumn 的集合映射时特别有用。

以下函数接受

  1. 引用 已连接集合或多值关联 的标识变量,或

  2. 一个 复合路径,它引用一个实体的集合或多值关联。

在情况 2 中,应用函数会产生隐式联接

函数 适用于 用途 JPA 标准

size()

任何集合

集合的大小

element()

任何集合

集合或列表的元素

index()

列表

列表元素的索引

key()

映射

映射项的键

value()

映射

映射项的值

entry() 💀

映射

映射中的整个项

下一组函数始终接受指向实体的集合或多值关联的复合路径。将其视为指向整个集合。

函数 适用于 用途 JPA 标准

elements()

任何集合

集合或列表的元素(整体)

indices()

列表

列表的索引(整体)

keys()

映射

映射的键(整体)

values()

映射

映射的值(整体)

其中一个函数的应用会产生隐式子查询或隐式联接。

此查询有一个隐式联接

select title, element(tags) from Book

此查询有一个隐式子查询

select title from Book where 'hibernate' in elements(tags)
对标识变量或单值路径表达式应用函数 elements()indices()keys()values() 没有任何意义。这些函数必须应用于对多值路径表达式的引用。
集合大小

size() 函数返回集合或多对多关联的元素数。

select name, size(books) from Author
集合或列表元素

element() 函数返回对已联接的集合或列表的某元素的引用。对于标识变量(如上例中的情况 1),此函数是可选的。对于复合路径(如上例中的情况 2),它是必需的。

列表索引

index() 函数返回对已联接列表的某个索引的引用。

在此示例中,element() 是可选的,但 index() 是必需的

select id(book), index(ed), element(ed)
from Book book as book
    join book.editions as ed
映射键和值

key() 函数返回对已联接映射的某个键的引用。value() 函数返回对其值的引用。

select key(entry), value(entry)
from Thing as thing
    join thing.entries as entry
集合量化

函数 elements()indices()keys()values() 用于对集合进行量化。我们可将其与下列内容一起使用

快捷方式 等效子查询

exists elements(book.editions)

exists (select ed from book.editions as ed)

2 in indices(book.editions)

2 in (select index(ed) from book.editions as ed)

10 > all(elements(book.printings))

10 > all(select pr from book.printings as pr)

max(elements(book.printings))

(从 book.printings 中选择 pr 的最大值)

例如

select title from Book where 'hibernate' in elements(tags)

不要将 elements() 函数和 element() 相混淆,不要将 indices() 函数和 index() 相混淆,不要将 keys() 函数和 key() 相混淆,不要将 values() 函数和 value() 相混淆。“单数”命名的函数处理“展开式”集合的元素。它们如果没有已经联接,就会向查询添加一个隐式联接。而“复数”命名的函数不会通过联接来展开集合。

以下查询是不同的

select title, max(index(revisions)) from Book  /* implicit join */
select title, max(indices(revisions)) from Book  /* implicit subquery */

第一个查询生成单行,而 max() 从所有书籍中获取。第二个查询每本书生成一行,而 max() 从属于给定书籍的集合元素中获取。

2.6.7. 用于处理 id 和版本的函数

最后,以下函数评估实体的 ID、版本或自然 ID,或一对一关联的外键

函数 用途 JPA 标准

id()

实体 @Id 属性的值。

version()

实体 @Version 属性的值。

naturalid()

实体 @NaturalId 属性的值。

fk()

@ManyToOne(或逻辑 @OneToOne)关联映射的外键列的值。使用带有 @NotFound 注释的关联时很有用。

2.6.8. 嵌入式 SQL 表达式

以下特殊函数允许我们嵌入对本机 SQL 函数的调用,直接引用某列或评估用本机 SQL 编写的表达式。

函数 用途 签名 JPA 标准

function()

调用 SQL 函数

function('fun', arg1, arg2)

function()

调用 SQL 函数

function(fun, arg1, arg2),
function(fun as Type, arg1, arg2)

column()

某列的值

column(entity.column),
column(entity.column as Type)

sql()

评估 SQL 表达式

sql('text', arg1, arg2)

在使用这些函数之一之前,问问你自己是否只需要使用本机 SQL 来写整个查询就可以了。
直接列引用

column() 函数允许我们引用表中未映射的某列。列名必须由一个标识变量或路径表达式限定。

select column(log.ctid as String)
from Log log

当然,表本身必须由一个实体类映射。

本机函数和用户定义函数

我们在上面描述的函数是 HQL 抽象的函数,可以跨数据库实现可移植性。但是,当然 HQL 无法抽象你的数据库中的每一个函数。

有几种方法可以调用本机或用户定义的 SQL 函数。

  • 本机或用户定义的函数可以使用 JPQL 的 function 语法调用,例如 function('sinh', phi),或 HQL 对该语法的扩展,例如 function(sinh as Double, phi)。(这是最简单的方式,但不是最好的方式。)

  • 用户编写的 FunctionContributor 可以注册用户定义函数。

  • 自定义 Dialect 可以通过覆盖 initializeFunctionRegistry() 注册其他本机函数。

注册函数并不难,但这超出了本文档的范围。

(甚至可以使用Hibernate提供的API来创建您自己的可移植函数!)

幸运的是,每个内置的Dialect已经为其支持的数据库注册了许多本机函数。

尝试将日志类别org.hibernate.HQL_FUNCTIONS设置为debug。然后Hibernate将在启动时记录所有注册函数类型签名的列表。

将本机SQL嵌入到HQL中

特殊函数sql()允许在HQL查询中使用本机SQL片段。

此函数的签名为sql(pattern[, argN]*),其中pattern必须是字符串文字,但其余参数可以是任何类型。模式文字未加引号,并且嵌入在生成的SQL中。模式中?的出现将用函数的其余参数替换。

例如,我们可以使用它来执行本机PostgreSQL类型转换

from Computer c where c.ipAddress = sql('?::inet', '127.0.0.1')

这会导致逻辑上等效的SQL

select * from Computer c where c.ipAddress = '127.0.0.1'::inet

或者我们可以使用本机SQL算子

from Human h order by sql('(? <-> ?)', h.workLocation, h.homeLocation)

此时,SQL在逻辑上等效于

select * from Human h where (h.workLocation <-> h.homeLocation)

2.7. 断言

谓词是一种运算符,当应用于某个参数时,它求值为truefalse。在SQL风格的三元逻辑世界中,我们必须扩展此定义以涵盖谓词求值为null的可能性。通常,当谓词的其中一个参数为null时,谓词将求值为null

谓词出现在where子句、having子句和搜索案例表达式中。

2.7.1. 比较运算符

二元比较运算符借用了SQL:=>>=<<=<>

如果您愿意,HQL将!=视为<>的同义词。

操作数应为同一种类型。

from Book where price < 1.0
from Author as author where author.nomDePlume <> author.person.name
select id, total
from (
    select ord.id as id, sum(item.book.price * item.quantity) as total
    from Order as ord
        join Item as item
    group by ord
)
where total > 100.0

2.7.2. between 断言

三元的between运算符及其否定not between确定某个值是否落在一个范围内。

当然,所有三个操作数都必须是兼容类型。

from Book where price between 1.0 and 100.0

2.7.3. 用于处理 null 的运算符

以下运算符使处理空值变得更加容易。这些谓词永远不会求值为null

运算符 否定 类型 语义

is null

is not null

一元后缀

如果左边的值为null,则为true,如果不是null,则为false

is distinct from

is not distinct from

二进制

如果左边的值等于右边的值,或者如果两个值均为null,则为true,否则为false

from Author where nomDePlume is not null

2.7.4. 用于处理布尔值的运算符

这些运算符对boolean类型的值执行比较。这些谓词永远不会求值为null

boolean基本类型的truefalse值不同于谓词产生的逻辑truefalse

有关谓词逻辑运算,请参阅以下逻辑运算符

运算符 否定 类型 语义

为真

不为真

一元后缀

如果左侧值为真,则为,否则为

为假

不为假

二进制

如果左侧值为假,则为,否则为

from Book where discontinued is not true

2.7.5. 集合断言

下述运算符适用于集合值属性和多对多关联。

运算符 否定 类型 语义

为空

不为空

一元后缀

如果左侧的集合或关联没有元素,则为

的成员

不是的成员

二进制

如果左侧值是右侧集合或关联的成员,则为

from Author where books is empty
select author, book
from Author as author, Book as book
where author member of book.authors

2.7.6. 字符串模式匹配

like运算符用于对字符串执行模式匹配。其姊妹ilike运算符用于执行不区分大小写的匹配。

它们的语法定义如下

expression "NOT"? ("LIKE" | "ILIKE") expression ("ESCAPE" character)?

右侧表达式是一个模式,其中

  • _匹配任何单个字符,

  • %匹配任意数量的字符,并且

  • 如果指定了转义字符,则可以用它转义这些通配符。

from Book where title not like '% for Dummies'

可选的escape字符允许模式包含一个文字_%字符。

如你所猜测的,not likenot ilikelikeilike的反义词,它们评估为完全相反的布尔值。

2.7.7. in 断言

in谓词评估结果为真,如果它左侧的值在…​嗯,其右侧找到的任何内容中。

它的语法非常复杂

expression "NOT"? "IN" inList

inList
	: collectionQuantifier "(" simplePath ")"
	| "(" (expression ("," expression)*)? ")"
	| "(" subquery ")"
	| parameter

HQL ANTLR 语法的这一不太美观的片段告诉我们,右侧的内容可能是

  • 用括号括起来的一个值列表,

  • 一个子查询,

  • 上面定义的用于处理集合的函数之一,或者

  • 一个查询参数,

左侧表达式的类型和右侧所有值的类型必须兼容。

JPQL 将合法类型限制为字符串、数字、日期/时间和枚举类型,并且在 JPQL 中,左侧表达式必须是

  • 一个状态字段,这意味着一个基础属性,不包括关联和嵌入式属性,或者

  • 一个实体类型表达式

HQL 的许可权更大。HQL 本身并未以任何方式限制类型,尽管数据库本身可能会限制。即使是嵌入式属性也被允许,尽管该特性取决于底层数据库中对元组或“行值”构造函数的支持级别。

from Payment as payment
where type(payment) in (CreditCardPayment, WireTransferPayment)
from Author as author
where author.person.name in (select name from OldAuthorData)
from Book as book
where :edition in elements(book.editions)

拥有一个参数化值列表非常常见。

这里有一个非常有用的惯用法

List<Book> books =
        session.createSelectionQuery("from Book where isbn in :isbns", Book.class)
            .setParameterList("isbns", listOfIsbns)
            .getResultList();

我们甚至可以“向量化”一个in谓词,使用元组构造函数和一个具有多个选择项的子查询

from Author as author
where (author.person.name, author.person.birthdate)
    in (select name, birthdate from OldAuthorData)

2.7.8. 比较运算符和子查询

我们在上面遇到的二元比较可能涉及一个量词,它可以是

  • 一个量化子查询,或者

  • 应用于在上面定义的函数之一的量词。

量词是一元前缀运算符:alleveryanysome

子查询运算符 同义词 语义

every

all

如果子查询结果集中的每一个值比较为 true,则计算结果为 true

any

some

如果子查询结果集中的至少有一个值比较为 true,则计算结果为 true

from Publisher pub where 100.0 < all(select price from pub.books)
from Publisher pub where :title = some(select title from pub.books)

2.7.9. exists 断言

一元前缀exists操作符如果其右侧的条件非空,则计算结果为 true。

其右侧的条件可以是

  • 子查询或

  • 上述定义的一个函数。

正如你猜想到的那样,如果右侧的条件为空,则not exists计算结果为 true。

from Author where exists elements(books)
from Author as author
where exists (
    from Order join items
    where book in elements(author.books)
)

2.7.10. 逻辑运算符

逻辑操作符是二元中缀andor以及一元前缀not

与 SQL 类似,逻辑算术基于三元逻辑。如果逻辑操作符具有 null 操作数,则计算结果为 null。

3. 根实体和联接

from子句及其从属join子句处于大多数查询的核心位置。

3.1. 声明根实体

from子句负责声明查询的其余部分可用的实体,并为其分配别名或使用 JPQL 规范的语言,标识变量

3.1.1. 标识变量

标识变量是我们用来引用查询中表达式中的实体及其属性的名称。它可以是任何合法的 Java 标识符。根据 JPQL 规范,标识变量必须被视为不区分大小写的语言元素。

标识变量实际上是可选的,但对于涉及多个实体的查询,几乎总是建议声明一个标识变量。

有效,但不是特别好的形式

from Publisher join books join authors join person where ssn = :ssn

可以使用as关键字声明标识变量,但这是可选的。

3.1.2. 根实体引用

根实体引用,或者 JPQL 规范称之为范围变量声明,是对映射的@Entity类型的直接引用,其中通过实体名称来引用。

记住,实体名称@Entity注解的name成员的值,或者默认情况下是不合格的 Java 类名称。

select book from Book as book

在此示例中,Book是实体名称,book是标识变量。as关键字是可选的。

或者,可以指定一个完全限定的 Java 类名称。然后,Hibernate 将查询继承命名类型的每个实体。

select doc from org.hibernate.example.AbstractDocument as doc where doc.text like :pattern

当然,可能有多个根实体。

select a, b
from Author a, Author b, Book book
where a in elements(book.authors)
  and b in elements(book.authors)

甚至可以使用cross join语法(代替逗号)来编写此查询

select a, b
from Book book
    cross join Author a
    cross join Author b
where a in elements(book.authors)
  and b in elements(book.authors)

当然,可以编写老式的 ANSI 前时代的连接

select book.title, publisher.name
from Book book, Publisher publisher
where book.publisher = publisher
  and book.title like :titlePattern

但是,我们从不以这种方式编写 HQL。

3.1.3. 多态性

HQL 和 JPQL 查询天生具有多态性。请考虑

select payment from Payment as payment

此查询明确给Payment实体命名。但是,CreditCardPaymentWireTransferPayment实体继承Payment,因此payment跨越了这三种类型。查询将返回所有这些实体的实例。

查询 from java.lang.Object 完全合法。(但没有什么用!)

它返回每个映射实体类型的每个对象。

3.1.4. 派生根

衍生根是发生在 from 子句中的非关联子查询。

select id, total
from (
    select ord.id as id, sum(item.book.price * item.quantity) as total
    from Order as ord
        join Item as item
    group by ord
)
where total > 100.0

衍生根可以声明标识变量。

select stuff.id, stuff.total
from (
    select ord.id as id, sum(item.book.price * item.quantity) as total
    from Order as ord
        join Item as item
    group by ord
) as stuff
where total > 100.0

此功能可用于将更复杂的查询分解成更小的部分。

我们强调衍生根必须是非关联子查询。它不能引用在同一个 from 子句中声明的其他根。

子查询也可出现在连接中,在此情况下,它可能关联子查询。

3.1.5. from 子句中的公共表表达式

公共表表达式 (CTE)类似于带名称的衍生根。我们将在稍后讨论 CTE。

3.2. 声明已联接的实体

通过关联或显式连接条件,连接允许我们从一个实体导航到另一个实体。以下内容是

  • 显式连接,在 from 子句中使用关键字 join 声明,以及

  • 隐式连接,无需在 from 子句中声明。

显式连接可能是

  • 内连接,写为 joininner join

  • 左外连接,写为 left joinleft outer join

  • 右外连接,写为 right joinright outer join,或

  • 全外连接,写为 full joinfull outer join

3.2.1. 显式根联接

显式根连接恰好像 SQL 中的 ANSI 风格连接。

select book.title, publisher.name
from Book book
    join Publisher publisher
        on book.publisher = publisher
where book.title like :titlePattern

连接条件已在 on 子句中明确写入。

这看起来很不错、很熟悉,但这不是 HQL 或 JPQL 中最常见的连接类型。

3.2.2. 显式关联联接

每个显式关联连接都指定要连接的实体属性。指定的属性

  • 通常是 @OneToMany@ManyToMany@OneToOne@ManyToOne 关联,但

  • 它可能是 @ElementCollection,并且

  • 它甚至可能是一个嵌入式类型的属性。

对于关联或集合,生成的 SQL 将具有同类型的连接。(对于多对多关联,它将有两个连接。)对于嵌入式属性,连接是纯逻辑的,并且不会导致在生成的 SQL 中进行连接。

显式连接可为连接的实体分配标识变量。

from Book as book
    join book.publisher as publisher
    join book.authors as author
where book.title like :titlePattern
select book.title, author.name, publisher.name

对于外连接,我们必须编写查询以适应连接关联可能缺失的可能性。

from Book as book
    left join book.publisher as publisher
    join book.authors as author
where book.title like :titlePattern
select book.title, author.name, ifnull(publisher.name, '-')

有关集合值关联引用的详细信息,请参见连接集合和多值关联

3.2.3. 具有联接条件的显式关联联接

withon 子句允许显示限定连接条件。

指定的连接条件被添加到由外键关联指定的连接条件中。这就是从历史上来说,HQL 在这里使用 keword with 的原因:“with” 强调新条件不会替换原有连接条件。

with 关键字特定于 Hibernate。JPQL 使用 on

出现于 withon 子句中的联接条件将添加到生成的 SQL 中的 on 子句。

from Book as book
    left join book.publisher as publisher
        with publisher.closureDate is not null
    left join book.authors as author
        with author.type <> COLLABORATION
where book.title like :titlePattern
select book.title, author.name, publisher.name

3.2.4. 关联提取

调用联接覆盖了给定关联的惰性,指定应该使用 SQL 联接调用关联。联接可能是内联接或外联接。

  • join fetch 或更明确地讲,inner join fetch 仅返回具有关联实体的基本实体。

  • left join fetch 或对冗长语言的爱好者来说,left outer join fetch 还会返回所有基本实体,包括没有关联的联接实体的基本实体。

这是 Hibernate 的最重要的特性之一。要使用 HQL 达到可接受的性能,经常需要使用 join fetch。如果没有它,你将很快遇到令人讨厌的“n+1 选择”问题。

例如,如果 Person 具有名为 phones 的一对多关联,在以下查询中使用 join fetch 指定应该在相同的 SQL 查询中调用集合元素

select book
from Book as book
    left join fetch book.publisher
    join fetch book.authors

在本示例中,我们对 book.publisher 使用左外联接,因为我们还想获取没有出版商的书,但对 book.authors 使用常规内联接,因为每本书至少有一个作者。

一个查询可能有多个调用联接,但请注意

  • 在一系列或并行查询中调用多个一对一关联绝对是安全的,

  • 一系列嵌套调用联接也很好,但

  • 并行中调用多个集合或多对多关联将导致数据库级别上的笛卡尔积,并且其性能可能很差。

HQL 不会禁止它,但通常最好是对 join fetched 实体应用限制,因为已调用的集合的元素是不完整的。实际上,最好避免给已调用的联接实体分配标识变量,除非目的是指定嵌套的调用联接。

在限制查询或分页查询中通常应该避免调用联接。这包括

  • 使用 QuerysetFirstResult()setMaxResults() 方法指定的限制执行的查询,或

  • 具有 HQL 中声明的限制或偏移量的查询,如下文 限制和偏移量 中所述。

也不得与 Query 接口的 scroll()stream() 方法一起使用。

在子查询中不允许调用联接,这毫无意义。

3.2.5. 具有类型转换的联接

使用 treat() 可以使用显式联接缩小联接实体的类型。

from Order as ord
    join treat(ord.payments as CreditCardPayment) as creditCardPayment
where length(creditCardPayment.cardNumber) between 16 and 20
select ord.id, creditCardPayment.cardNumber, creditCardPayment.amount

在此,在 `treat()` 右侧声明的标识变量 `ccp` 的类型已缩小,变为 `CreditCardPayment`,而非声明类型 `Payment`。这允许在查询的剩余部分中引用由子类型 `CreditCardPayment` 声明的属性 `cardNumber`。

有关 `treat()` 的更多信息,请参见 类型和类型转换

3.2.6. 联接中的子查询

一个 `join` 子句可能包含一个子查询,它可能是

  • 一个无关联子查询,它几乎等同于一个 派生根,不同之处在于它可能有 `on` 限制,或者

  • 一个横向连接,它是一个关联子查询,并且可能引用同一 `from` 子句中先前声明的其他根。

关键字 `lateral` 只是区分了这两种情况。

from Phone as phone
    left join (
        select call.duration as duration, call.phone.id as cid
        from Call as call
        order by call.duration desc
        limit 1
    ) as longest on cid = phone.id
where phone.number = :phoneNumber
select longest.duration

此查询也可以使用 `lateral` 连接来表示

from Phone as phone
    left join lateral (
       select call.duration as duration
       from phone.calls as call
       order by call.duration desc
       limit 1
    ) as longest
where phone.number = :phoneNumber
select longest.duration

横向连接可以是内部连接或左外连接,但不能是右连接或全连接。

传统 SQL 不允许在 `from` 子句中使用关联子查询。横向连接实质上就是这样,但采用了一种你可能想不到的不同语法。

在某些数据库中,`join lateral` 编写为 `cross apply`。在 Postgres 中,它就是纯粹的 `lateral`,没有 `join`。

就好像他们故意要混淆我们一样。

横向连接对于计算多个组的顶部 N 个元素特别有用。

大多数数据库都支持某种形式的 `join lateral`,而 Hibernate 为不支持该特性的数据库模拟该特性。但是模拟既不高效也不支持所有可能的查询形状,因此在目标数据库上进行测试非常重要。

3.2.7. 隐式关联联接(路径表达式)

不必显式 `join` 每个出现在查询中的实体。相反,可以导航实体关联,就像在 Java 中一样

  • 如果一个属性是嵌入式类型,或者是一对一关联,则可以进一步导航,但是

  • 如果一个属性是基本类型,则它被认为是终端,并且不能进一步导航,并且

  • 如果一个属性是集合值,或者是一对多关联,则它可以被导航,但只能在 `value()`, `element()` 或 `key()` 的帮助下。

很明显

  • 仅包含两个元素的路径表达式,例如 `author.name`,仅指由在 `from` 或 `join` 中定义的别名 `author` 的实体直接保存的状态。

  • 但更长的路径表达式,例如 `author.person.name`,可能指由相关实体保存的状态。(或者,它可能指由嵌入类保存的状态。)

在第二种情况下,如果必要,Hibernate 会自动向生成的 SQL 添加一个连接。

from Book as book
where book.publisher.name like :pubName

如本例所示,隐式连接通常出现在 HQL 查询的 `from` 子句外部。但是,它们始终会影响 SQL 查询的 `from` 子句。

上面的示例等同于

select book
from Book as book
    join book.publisher as pub
where pub.name like :pubName

请注意

  • 隐式连接总是被视为内部连接。

  • 同一隐式联接的多次出现始终引用同一 SQL 联接。

此查询

select book
from Book as book
where book.publisher.name like :pubName
  and book.publisher.closureDate is null

仅生成一个 SQL 联接,并且只是编写

select book
from Book as book
    join book.publisher as pub
where pub.name like :pubName
  and pub.closureDate is null

3.2.8. 联接集合和多值关联

当联接涉及集合或多值关联时,声明的标识变量引用集合的元素,即

  • Set 的元素,

  • List 的元素,而不是它们在列表中的索引,或者

  • Map 的值,而不是它们的键。

select publisher.name, author.name
from Publisher as publisher
    join publisher.books as book
    join book.authors author
where author.name like :namePattern

在此示例中,标识变量 author 是类型为 Author,列表 Book.authors 的元素类型。但如果我们需要引用列表中 Author 的索引,则需要一些额外的语法。

您可能还记得我们稍早提到过列表索引Map 键和值。这些函数可以应用于在集合联接或多值关联联接中声明的标识变量。

函数 适用于 解释 备注

value()element()

任何集合

集合元素或映射条目值

通常可选。

index()

任何带有索引列的 List

列表中元素的索引

为了提高兼容性,当应用于映射时,它也是 key() 的备用选项。

key()

任何 Map

列表中条目的键

如果键为实体类型,则可以进一步导航。

entry()

任何 Map

映射条目,即键和值的 Map.Entry

仅作为终端路径合法,并且仅允许在 select 子句中。

特别是,index()key() 获取对列表索引或映射键的引用。

select book.title, author.name, index(author)
from Book as book
    join book.authors as author
select publisher.name, leadAuthor.name
from Publisher as publisher
    join publisher.books as book
    join book.authors leadAuthor
where leadAuthor.name like :namePattern
  and index(leadAuthor) == 0

3.2.9. 涉及集合的隐式联接

诸如 book.authors.name 的路径表达式不被视为合法。我们不能只使用此语法导航多值关联。

相反,函数 element()index()key()value() 可应用于路径表达式,以表达隐式联接。因此,我们必须写 element(book.authors).nameindex(book.authors)

select book.title, element(book.authors).name, index(book.authors)
from Book book

可以使用索引运算符识别索引集合(数组、列表或映射)的元素

select publisher.name, book.authors[0].name
from Publisher as publisher
    join publisher.books as book
where book.authors[0].name like :namePattern

4. 选择、投影和聚合

联接是一种关系操作。它是一种根据其他关系产生关系(表)的操作。此类操作合在一起构成关系代数

我们现在必须了解该系列的其余部分:限制别名选择、投影、聚合、并集/交集以及最后是排序和限制,这些操作严格来讲不是关系演算的一部分,但是由于非常有用,所以通常会一起执行。

我们从最容易理解的操作开始。

4.1. 限制

where 子句限制 select 查询返回的结果或限制 updatedelete 查询的作用域。

此操作通常称为选择,但由于该术语经常与选择关键字混淆,并且投影和选择都涉及“选择”内容,因此我们在这里将使用不太模糊的术语限制

限制仅是单个逻辑表达式,这是一个我们在 谓词 中已经详述的主题。因此,我们将快速进入下一个更有趣的操作。

4.2. 聚合

聚合查询是在其投影列表中包含聚合函数的查询。它将多行折叠成一行。聚合查询用于汇总和分析数据。

聚合查询可能有一个按组子句。按组子句将结果集划分为组,以便在选择列表中带聚合函数的查询不会返回整个查询的单个结果,而是每个组的一个结果。如果聚合查询没有一个按组子句,它总是生成单行结果。

简而言之,分组控制聚合的效果。

带聚合的查询也可以有一个哈维恩子句,它对组施加了限制。

4.2.1. 聚合和分组

按组子句看起来与选择子句非常相似——它有一个分组项列表,但是

  • 如果只有一个项目,则查询将对该项目的每个唯一值生成单个结果,或者

  • 如果有多个项目,则查询将对每个唯一组合或其值生成结果。

分组项的 BNF 仅仅是

identifier | INTEGER_LITERAL | expression

考虑以下查询

select book.isbn,
    sum(quantity) as totalSold,
    sum(quantity * book.price) as totalBilled
from Item
where book.isbn = :isbn
select book.isbn,
    year(order.dateTime) as year,
    sum(quantity) as yearlyTotalSold,
    sum(quantity * book.price) as yearlyTotalBilled
from Item
where book.isbn = :isbn
group by year(order.dateTime)

第一个查询计算给定年份中所有订单的总计。第二个查询计算每年的总计,前提是按年份对订单进行分组。

4.2.2. 总计和小计

如果数据库支持,则可以在按组子句中使用特殊函数rollup()cube()。语义与 SQL 相同。

这些函数对报表特别有用。

  • 带有rollup()按组子句用于生成小计和总计。

  • 带有cube()按组子句允许对每列组合计算总计。

4.2.3. 聚合和限制

在分组查询中,where子句适用于非聚合值(它确定哪些行将进入聚合)。having子句也限制结果,但它对聚合值进行操作。

上面的示例 中,我们计算了有数据可用的每年的总计。但我们的数据集可能延伸到更遥远的过去,甚至可以追溯到 Hibernate 2.0 之前的黑暗时代。因此,让我们将我们的结果集限制在我们自己文明时代的数据

select book.isbn,
    year(order.dateTime) as year,
    sum(quantity) as yearlyTotalSold,
    sum(quantity * book.price) as yearlyTotalBilled
from Item
where book.isbn = :isbn
group by year(order.dateTime)
having year(order.dateTime) > 2003
   and sum(quantity) > 0

having 子句遵循与 where 子句相同的规则,也仅仅是一个逻辑谓词。having 限制在分组和聚合已经执行之后应用,而 where 子句在数据分组或聚合之前应用。

4.3. 投影

select 列表标识要作为查询结果返回哪些对象和值。此操作称为投影

selectClause
    : "SELECT" "DISTINCT"? selection (","" selection)*

除非另有说明,否则 Expressions 中讨论的任何表达式类型都可能出现在投影列表中。

如果没有明确的 select 列表,那么正如我们在 前面 所看到的,投影从 from 子句中发生的实体和连接推断而来,以及 createQuery() 调用指定的结果类型。但在最简单的情形中,最好显式指定投影。

4.3.1. 删除重复项

distinct 关键字有助于从查询结果列表中删除重复的结果。其唯一效果是向生成的 SQL 添加 distinct

select distinct lastName from Person
select distinct author
from Publisher as pub
    join pub.books as book
    join book.authors as author
where pub.id = :pid

从 Hibernate 6 开始,由使用 join fetch 产生的重复结果会自动由 Hibernate 在内存中(在读取数据库结果和将实体实例实现为 Java 对象之后)删除。不再需要显式删除重复结果,特别是,distinct 不应为此目的使用。

4.3.2. 聚合函数

在 select 列表中使用 count()sum()max() 等聚合函数很常见。聚合函数是可缩小结果集大小的特殊函数。

ANSI SQL 和 JPQL 中定义的标准聚合函数如下:

聚合函数 参数类型 结果类型 JPA 标准 / ANSI SQL 标准

count(),包括 count(distinct)count(all)count(*)

任意

Long

✔/✔

avg()

任意数字类型

Double

✔/✔

min()

任意数字类型或字符串

与参数类型相同

✔/✔

max()

任意数字类型或字符串

与参数类型相同

✔/✔

sum()

任意数字类型

参见下表

✔/✔

var_pop()var_samp()

任意数字类型

Double

✖/✔

stddev_pop()stddev_samp()

任意数字类型

Double

✖/✔

select count(distinct item.book)
from Item as item
where year(item.order.dateTime) = :year
select sum(item.quantity) as totalSales
from Item as item
where item.book.isbn = :isbn
select
    year(item.order.dateTime) as year,
    sum(item.quantity) as yearlyTotal
from Item as item
where item.book.isbn = :isbn
group by year(item.order.dateTime)
select
    month(item.order.dateTime) as month,
    avg(item.quantity) as monthlyAverage
from Item as item
where item.book.isbn = :isbn
group by month(item.order.dateTime)

对于 sum(),分配结果类型的规则为:

参数类型 结果类型

BigInteger 之外的任意整数数字类型

Long

任意浮点数数字类型

Double

BigInteger

BigInteger

BigDecimal

BigDecimal

HQL 定义了两个附加的聚合函数,它们接受一个逻辑谓词作为参数。

聚合函数 参数类型 结果类型 JPA 标准

any()some()

逻辑谓词

Boolean

every()all()

逻辑谓词

Boolean

例如,我们可以编写 every(p.amount < 1000.0)

下面我们将介绍 有序集聚合函数

聚合函数通常出现在 select 子句中,但对聚合的控制是 group by 子句的职责,如 下面 所述。

4.3.3. 聚合函数和集合

我们前面已经接触过的 elements()indices() 函数,它允许我们对一个集合应用聚集函数

新语法 旧版 HQL 函数 💀 适用于 用途

max(elements(x))

maxelement(x)

任何带有可排序元素的集合

集合的最大元素或映射值

min(elements(x))

minelement(x)

任何带有可排序元素的集合

集合的最小元素或映射值

sum(elements(x))

任何带有数字元素的集合

集合的元素之和或映射值之和

avg(elements(x))

任何带有数字元素的集合

集合的元素之和或映射值之和的平均值

max(indices(x))

maxindex(x)

已编索引的集合(列表和映射)

集合的最大列表索引或映射键

min(indices(x))

minindex(x)

已编索引的集合(列表和映射)

集合的最小列表索引或映射键

sum(indices(x))

已编索引的集合(列表和映射)

集合的列表索引或映射键之和

avg(indices(x))

已编索引的集合(列表和映射)

集合的列表索引或映射键的平均值

这些操作在使用 @ElementCollection 时通常非常有用。

select title, max(indices(authors))+1, max(elements(editions)) from Book

4.3.4. 带有限制的聚合函数

所有聚集函数都支持包含一个过滤器子句,它是一种小型 where,将一个限制应用到选择列表中的一个元素

select
    year(item.order.dateTime) as year,
    sum(item.quantity) filter (where not item.order.fulfilled) as unfulfilled,
    sum(item.quantity) filter (where item.order.fulfilled) as fulfilled,
    sum(item.quantity * item.book.price) filter (where item.order.paid)
from Item as item
where item.book.isbn = :isbn
group by year(item.order.dateTime)

filter 的 BNF 语法非常简单

filterClause
	: "FILTER" "(" "WHERE" predicate ")"

4.3.5. 有序集合聚合函数

有序集合聚集函数是一种特殊的聚集函数,它不仅

  • 像上面一样带有可选的过滤器子句,还

  • 带有包含小型 order by 规范的 within group 子句。

within group 的 BNF 语法很简单

withinGroupClause
	: "WITHIN" "GROUP" "(" "ORDER" "BY" sortSpecification ("," sortSpecification)* ")"

有序集合聚集函数有两种主要类型

  • 反分布函数,它能计算出表征组中值的分布的一个值;例如,percentile_cont(0.5) 是中位数,percentile_cont(0.25) 是下四分位数。

  • 假设集合函数,它能确定一个“假设”值在有序值集合中的位置。

以下有序集合聚集函数在很多平台上都可以使用

类型 函数

逆分布函数

mode()percentile_cont()percentile_disc()

假设集合函数

rank()dense_rank()percent_rank()cume_dist()

其他

listagg()

此查询计算一本图书的中位价格

select percentile_cont(0.5)
    within group (order by price)
from Book

此查询查找价格低于 10 美元的图书的所占比例

select 100 * percent_rank(10.0)
    within group (order by price)
from Book

实际上,支持最广泛的有序集合聚集函数是通过连接组内的值来构建一个字符串的函数。此函数在不同的数据库中名称不同,但 HQL 抽象出这些差异,并且遵循 ANSI SQL 的做法,将它称为 listagg()

select listagg(title, ', ')
    within group (order by isbn)
from Book
group by element(authors)

此非常有用的函数通过连接参数的聚集值来生成一个字符串。

4.3.6. 窗口函数

窗口函数也是带有 over 子句的函数,例如

select
    item.order.dateTime,
    sum(item.quantity)
        over (order by item.order.dateTime)
        as runningTotal
from Item item

此查询返回随时间推移的销售运行总计。换句话说,sum() 计算结果集当前行的窗口和所有以前的行。

窗口函数应用程序可以根据需要指定以下任何子句

可选子句 关键字 用途

结果集的分区

按分区

按组 非常相似,但不会将每个分区折叠成一行

分区的顺序

order by

指定分区内行的顺序

计算窗口

rangerowsgroups

在分区内定义窗口框架的边界

限制

filter

作为聚合函数,窗口函数可以根据需要指定过滤器

例如,我们可以按账本对 running total 进行分区

select
    item.book.isbn,
    item.order.dateTime,
    sum(item.quantity)
        over (partition by item.book
              order by item.order.dateTime)
        as runningTotal
from Item item

每个分区独立运行,换句话说,行无法跨分区泄漏。

窗口函数应用程序的完整语法极其复杂,这一点通过此 BNF 得以体现

overClause
	: "OVER" "(" partitionClause? orderByClause? frameClause? ")"

partitionClause
	: "PARTITION" "BY" expression ("," expression)*

frameClause
	: ("RANGE"|"ROWS"|"GROUPS") frameStart frameExclusion?
	| ("RANGE"|"ROWS"|"GROUPS") "BETWEEN" frameStart "AND" frameEnd frameExclusion?

frameStart
	: "CURRENT" "ROW"
	| "UNBOUNDED" "PRECEDING"
	| expression "PRECEDING"
	| expression "FOLLOWING"

frameEnd
	: "CURRENT" "ROW"
	| "UNBOUNDED" "FOLLOWING"
	| expression "PRECEDING"
	| expression "FOLLOWING"

frameExclusion
	: "EXCLUDE" "CURRENT" "ROW"
	| "EXCLUDE" "GROUP"
	| "EXCLUDE" "TIES"
	| "EXCLUDE" "NO" "OTHERS"

窗口函数与聚合函数类似,因为它们基于包含多行的“框架”计算一些值。但与聚合函数不同,窗口函数不会展平窗口框架内的行。

窗口框架

窗口框架是给定分区内传递给窗口函数的行集合。结果集的每行都有一个不同的窗口框架。在我们的示例中,窗口框架包含分区内所有前行,即所有 item.book 相同且 item.order.dateTime 较早的行。

窗口框架的边界通过计算窗口子句控制,该子句可以指定以下模式之一

模式 定义 示例 解释

rows

由给定行数定义的框架边界

5 preceding

分区中的前 5 行

groups

由给定同级组数定义的框架边界,如果同级组通过 order by 分配了相同位置,则该组的行属于同一同级组

5 preceding

分区中前 5 个同级组中的行

range

order by 中用于表达式value的最大差异定义的框架边界

range between 1.0 preceding and 1.0 following

order by 表达式与当前行相差的最大绝对值为 1.0 的行

框架排除子句允许排除当前行周围的行

选项 解释

exclude current row

排除当前行

exclude group

排除当前行的同级组行

exclude ties

不包含当前行的对等组中的行,除了当前行

不排除其他任何行

默认值,不会排除任何内容

在默认情况下,窗口框架定义为在无界的前置行与当前行之间排除其他任何行,这意味着包括当前行在内的每行。

模式rangegroups,连同框架排除模式,并非在所有数据库上都可用。

广泛支持的窗口函数

以下窗口函数在所有主流平台上都可用

窗口函数 用途 签名

row_number()

当前行与其框架内的位置

row_number()

lead()

框架中后续行的值

lead(x), lead(x, i, x)

lag()

框架中先前行的值

lag(x), lag(x, i, x)

first_value()

框架中第一行的值

first_value(x)

last_value()

框架中最后一行值

last_value(x)

nth_value()

框架中第 `n` 行的值

nth_value(x, n)

原则上,也可能将每个聚合函数或有序集合聚合函数用作窗口函数,仅需通过指定over,但并非每个函数都支持所有数据库。

窗口函数和有序集合聚合函数并非在所有数据库上都可用。即使在可用的情况下,特定功能的支持在数据库之间也存在很大差异。因此,我们不会在这里浪费时间深入展开讨论。有关这些函数的语法和语义的更多信息,请查阅您所用 SQL 方言的文档。

4.4. 结果集上的操作

这些运算符适用于整个结果集,不适用于表达式

  • unionunion all,

  • intersectintersect all,以及

  • exceptexcept all

与在 SQL 中一样,all禁止消除重复的结果。

select nomDePlume from Author where nomDePlume is not null
union
select name from Person

4.5. 排序

默认情况下,按任意顺序返回查询结果。

对集合施加顺序称为排序

关系(数据库表)是一个集合,因此,某些特别固执己见的纯粹主义者认为,排序在关系的代数中没有位置。我们认为这有些可笑: 实用的数据分析几乎总是涉及排序,而排序是一项定义十分明确的操作。

order by 子句指定用于对结果进行排序的投影项目列表。每个排序的项目可能是

  • 实体或可嵌入类的属性,

  • 更复杂的表达式,

  • 在选择列表中声明的投影项目的别名,或

  • 一个字面整数,指示选择列表中投射项的序数位置。

当然,原则上,只有特定类型才能排序:数字类型、字符串以及日期和时间类型。但 HQL 在此处非常宽松,并允许几乎任何类型的表达式出现在排序列表中。具有可排序标识符类型的实体的标识符变量甚至可以作为排序项出现。

JPQL 规范要求order by 子句中的每个已排序项也出现在select 子句中。HQL 并未强制此限制,但希望实现数据库可移植性的应用程序应注意,某些数据库确实执行此限制。

因此,你可能希望避免在排序列表中使用复杂表达式。

排序项的 BNF 为

sortExpression sortDirection? nullsPrecedence?

sortExpression
    : identifier | INTEGER_LITERAL | expression

sortDirection
    : "ASC" | "DESC"

nullsPrecedence
    : "NULLS" ("FIRST" | "LAST")

order by 子句中列出的每个已排序项可以明确指定一个方向,即

  • asc 表示升序,或

  • desc 表示降序。

如果未明确指定方向,则结果将按升序返回。

当然,关于 null 值存在歧义。因此,可以明确指定 null 值的排序

优先级 解释

null 值最先

将 null 值放在结果集的开头

null 值最后

将它们放在结尾

select title, publisher.name
from Book
order by title, publisher.name nulls last
select book.isbn,
    year(order.dateTime) as year,
    sum(quantity) as yearlyTotalSold,
    sum(quantity * book.price) as yearlyTotalBilled
from Item
where book.isbn = :isbn
group by year(order.dateTime)
having year(order.dateTime) > 2000
   and sum(quantity) > 0
order by yearlyTotalSold desc, year desc

具有已排序结果列表的查询可能有限制或分页。

4.5.1. 限制和偏移

对查询返回的结果数量设置一个上限通常是有用的。limitoffset 子句分别替代了 setMaxResults()setFirstResult() 的使用,并且可以类似地用于分页。

如果 limitoffset 带有参数,则使用 setMaxResults()setFirstResult() 会容易得多。

SQL fetch 语法作为替代受支持

简短形式 详细形式 用途

limit 10

fetch first 10 rows only

限制结果集

limit 10 offset 20

offset 20 rows fetch next 10 rows only

对结果集进行分页

BNF 变得有点复杂

limitClause
    : "LIMIT" parameterOrIntegerLiteral

offsetClause
    : "OFFSET" parameterOrIntegerLiteral ("ROW" | "ROWS")?

fetchClause
    : "FETCH" ("FIRST" | "NEXT")
      (parameterOrIntegerLiteral | parameterOrNumberLiteral "%")
      ("ROW" | "ROWS")
      ("ONLY" | "WITH" "TIES")

这两个查询相同

select title from Book
order by title, published desc
limit 50
select title from Book
order by title, published desc
fetch first 50 rows only

这些是明确定义的限制:数据库返回的结果数量将限制在 50 个,就像所承诺的那样。但并非每个查询都是如此。

限制肯定不是一个明确定义的关系操作,并且必须谨慎使用。

特别是,限制与外连接提取不兼容。

HQL 允许此下一个查询,并且getResultList() 返回的结果不超过 50 个,就像预期一样

select title from Book
    join fetch authors
order by title, published desc
limit 50

但是,如果你记录 Hibernate 执行的 SQL,你将注意到一些错误

select
    b1_0.isbn,
    a1_0.books_isbn,
    a1_0.authors_ORDER,
    a1_1.id,
    a1_1.bio,
    a1_1.name,
    a1_1.person_id,
    b1_0.price,
    b1_0.published,
    b1_0.publisher_id,
    b1_0.title
from
    Book b1_0
join
    (Book_Author a1_0
        join
            Author a1_1
                on a1_1.id=a1_0.authors_id)
        on b1_0.isbn=a1_0.books_isbn
order by
    b1_0.title,
    b1_0.published desc

limit 子句发生了什么?

当范围或分页与内部联接相结合使用时,Hibernate必须从数据库中检索所有匹配结果,并在内存中应用范围

几乎可以肯定与您预期的行为不符,而且通常会表现出极差的性能特征。

4.6. 公共表表达式

可以将公用表表达式或CTE视为一种命名的子查询。原则上,带有非关联子查询的任何查询都可以重写,使子查询出现在with子句中。

但是,CTE具有子查询所没有的功能。with子句允许我们

  • 指定物化提示,以及

  • 编写递归查询。

对于本机不支持CTE的数据库,Hibernate会尝试将具有CTE的任何HQL查询重写为具有子查询的SQL查询。对于递归查询,很遗憾,这是不可能的。

让我们快速了解一下BNF

withClause
	: "WITH" cte ("," cte)*

cte
	: identifier AS ("NOT"? "MATERIALIZED")? "(" queryExpression ")"
      searchClause? cycleClause?

with子句位于查询的开头。它可以声明具有不同名称的多个CTE。

with
    paid as (
        select ord.id as oid, sum(payment.amount) as amountPaid
        from Order as ord
            left join ord.payments as payment
        group by ord
        having local datetime - ord.dateTime < 365 day
    ),
    owed as (
        select ord.id as oid, sum(item.quantity*item.book.price) as amountOwed
        from Order as ord
            left join ord.items as item
        group by ord
        having local datetime - ord.dateTime < 365 day
    )
select id, paid.amountPaid, owed.amountOwed
from Order
where paid.amountPaid < owed.amountOwed
  and paid.oid = id and owed.oid = id

请注意,如果我们使用子查询重写此查询,它看起来会笨拙得多。

4.6.1. 具体化提示

materialized关键字是有关数据库的提示,即子查询应单独执行,其结果应存储在临时表中。

另一方面,其死对头not materialized是一个提示,即子查询应内联显示在每个使用位置,并对每个用法独立进行优化。

物化提示的确切影响取决于平台。

上例中的查询几乎没有变化。我们只需将materialized添加到CTE声明中。

with
    paid as materialized (
        select ord.id as oid, sum(payment.amount) as amountPaid
        from Order as ord
            left join ord.payments as payment
        group by ord
        having local datetime - ord.dateTime < 365 day
    ),
    owed as materialized (
        select ord.id as oid, sum(item.quantity*item.book.price) as amountOwed
        from Order as ord
            left join ord.items as item
        group by ord
        having local datetime - ord.dateTime < 365 day
    )
select id, paid.amountPaid, owed.amountOwed
from Order
where paid.amountPaid < owed.amountOwed
  and paid.oid = id and owed.oid = id

4.6.2. 递归查询

递归查询是CTE自引用定义的查询。递归查询遵循非常特定的模式。CTE定义为以下项的并集

  • 返回一组初始行的基础子查询,递归从该行开始,

  • 通过联接CTE本身返回附加行的递归执行子查询。

让我们通过一个示例演示一下。

首先,我们需要某种树形实体

@Entity
class Node {
	@Id Long id;
	String text;
	@ManyToOne Node parent;
}

我们可以通过以下递归查询获取Node的树

with Tree as (
    /* base query */
    select root.id as id, root.text as text, 0 as level
        from Node root
        where root.parent is null
    union all
    /* recursion */
    select child.id as id, child.text as text, level+1 as level
        from Tree parent
        join Node child on child.parent.id = parent.id
)
select text, level
from Tree

查询树形数据结构时,基础子查询通常返回根节点或节点。递归执行的子查询返回当前节点集的子项。它使用前一次执行的结果重复执行。当递归执行的子查询不再返回新节点时,递归终止。

Hibernate无法在不支持递归查询的数据库上模拟递归查询。

现在,如果一张图包含循环,即它不是一棵树,那么递归可能永远不会终止。

4.6.3. 循环检测

cycle子句启用了环路检测,并且如果两次遇到同一个节点,将中止递归。

with Tree as (
    /* base query */
    select root.id as id, root.text as text, 0 as level
        from Node root
        where root.parent is null
    union all
    /* recursion */
    select child.id as id, child.text as text, level+1 as level
        from Tree parent
        join Node child on child.parent.id = parent.id
) cycle id set abort to 'aborted!' default ''  /* cycle detection */
select text, level, abort
from Tree
order by level

此处

  • id列用于检测循环,并且

  • 如果检测到循环,则abort列设置为字符串值'aborted!'

Hibernate 在本机不支持的数据库上模拟了 cycle 从句。

用于 cycle 的 BNF 是

cycleClause
    : "CYCLE" identifier ("," identifier)*
      "SET" identifier ("TO" literal "DEFAULT" literal)?
      ("USING" identifier)?

using 任意指定的列保留指向当前行的路径。

4.6.4. 以深度优先或广度优先方式排序

通过 search 从句可以控制按深层优先递归搜索或广度优先递归搜索返回查询结果。

在上面的查询中,我们明确编码了保留递归深度的 level 列,并根据此深度对结果集排序。借助 search 从句,此类记录已针对我们进行妥善处理。

对于深度优先搜索,我们有

with Tree as (
    /* base query */
    select root.id as id, root.text as text
        from Node root
        where root.parent is null
    union all
    /* recursion */
    select child.id as id, child.text as text
        from Tree parent
        join Node child on child.parent.id = parent.id
) search depth first by id set level  /* depth-first search */
from Tree
select text
order by level

对于广度优先搜索,我们只需要更改一个关键字

with Tree as (
    /* base query */
    select root.id as id, root.text as text
        from Node root
        where root.parent is null
    union all
    /* recursion */
    select child.id as id, child.text as text
        from Tree parent
        join Node child on child.parent.id = parent.id
) search breadth first by id set level  /* breadth-first search */
from Tree
select text
order by level desc

Hibernate 在本机不支持的数据库上模拟了 search 从句。

用于 search 的 BNF 是

searchClause
    : "SEARCH" ("BREADTH"|"DEPTH") "FIRST"
      "BY" searchSpecifications
      "SET" identifier

searchSpecifications
    : searchSpecification ("," searchSpecification)*

searchSpecification
    : identifier sortDirection? nullsPrecedence?

5. 致谢

可在GitHub 信息库中找到向 Hibernate ORM 做出贡献的完整列表。

以下是参与此文档撰写的贡献者

  • Gavin King