针对Hibernate的标准API的扩展

374 阅读9分钟

大多数开发者都知道JPA规范定义了基于字符串的JPQL查询语言,Hibernate对其进行了扩展,以支持诸如数据库特定函数、窗口函数和基于集合的操作。但大多数开发者不知道,从第6版开始,Hibernate也为JPA的标准API做了同样的事情。

当然,扩展一个API要比为基于字符串的查询语言做同样的事情要复杂一些。为了扩展JPQL的功能,Hibernate团队只需要为查询字符串的解析器添加更多的功能,而不需要改变任何官方的API。Criteria API的扩展需要额外的接口和返回这些接口的新方法。

Hibernate 6通过提供HibernateCriteriaBuilder接口来处理这个问题,该接口扩展了JPA的CriteriaBuilder 接口,并在其专有的Session接口中添加了一个方法来获取HibernateCriteriaBuilder实例。

内容

HibernateCriteriaBuilder 接口

在我们谈论HibernateCriteriaBuilder接口之前,我们需要退一步,看看如何创建一个标准的CriteriaQuery。之后,我将向你展示如何获得HibernateCriteriaBuilder 以及它为JPA的标准Criteria API增加的功能。

使用JPA的CriteriaBuilder 接口

使用标准API的第一步总是调用EntityManager 接口的getCriteriaBuilder 方法。该方法返回JPA的CriteriaBuilder的实例,你可以用它来创建查询的不同部分。在下面的代码片断中,我使用它来创建一个非常基本的查询,以返回所有棋手 用名字以 "anssen "结尾的白色棋子下的ChessGame 实体。

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

CriteriaBuilder cBuilder = em.getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));

em.getTransaction().commit();
em.close();

你可以看到,我在两个地方使用了JPA的CriteriaBuilder

  1. 创建一个CriteriaQuery 对象,代表一个返回ChessGame 对象的查询。
  2. 为查询的WHERE子句创建一个类似的谓词,检查ChessGameplayerWhite 属性是否像"%anssen"

JPA的CriteriaBuilder 接口提供了很多其他的方法,你可以用来实例化不同种类的CriteriaQuery对象,建立更复杂的WHERE子句,以及调用数据库函数。我在Persistence Hub中的Advanced Hibernate课程中更详细地解释了所有这些,你可以在官方Javadoc中找到所有方法的完整列表。

如何获得一个HibernateCriteriaBuilder 实例

Hibernate的HibernateCriteriaBuilder 接口扩展了JPA的CriteriaBuilder 接口。由于这个原因,HibernateCriteriaBuilder的实现支持相同的方法,你可以用我在上一节中展示的相同方式来使用它。除此之外,该接口还定义了一些专有的方法来支持诸如设置操作和额外的数据库功能。

在你的代码中,你将认识到的主要区别是你如何实例化一个HibernateCriteriaBuilder。最好的方法是通过调用Hibernate会话 接口的getCriteriaBuilder方法来实例化它。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));

你也可以将一个CriteriaBuilder接口投给HibernateCriteriaBuilder。当然,这种转换不是类型安全的,它依赖于Hibernate使用同一个类来实现这两个接口的实现细节。因此,我建议你获得一个Session 并调用getCriteriaBuilder 方法。

HibernateCriteriaBuilder cBuilder = (HibernateCriteriaBuilder) em.getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));

HibernateCriteriaBuilder增加的功能

正如你在HibernateCriteriaBuilder 接口的官方Javadoc中看到的,该接口定义了许多方法来构建查询的不同部分。其中一些是由JPA的CriteriaBuilder定义的;另一些是Hibernate特有的功能。下面是HibernateCriteriaBuilder 接口定义的一些最有趣的附加功能。

插入到选择语句

INSERT INTO SELECT语句是一个著名的SQL特性,它使你能够将查询选择的数据作为新记录插入到数据库表中。从版本6开始,Hibernate就支持HQL语句,Hibernate6.1将提供这个功能作为标准API的一个扩展。

附加表达式

HibernateCriteriaBuilder定义了几种创建表达式的方法,你可以用它们来执行计算、转换或提取信息以及获得当前日期或时间。下面是几个例子。

  • JpaExpression sign(Expression<? extends Number> x)
    如果提供的参数是正数,则返回1,如果是负数则返回-1,如果正好是0则返回0。
  • JpaExpression ceiling(Expression x)
    返回大于或等于所提供参数的最小的整数。
  • JpaExpression floor(Expression x)
    返回小于或等于所提供参数的最小的最大整数。
  • JpaExpression round(Expression x, Integer n)
    返回第一参数四舍五入到第二参数的小数位数。
  • JpaExpression exp(Expression x)andJpaExpression power(Expression x, Expression y)
    返回欧拉数e提高到所提供参数的幂,或者返回第一参数提高到第二参数的幂。
  • JpaExpression ln(Expression<? extends Number> x)
    返回所提供参数的自然对数。
  • JpaExpression<java.time.LocalDate> localDate(),JpaExpression localDateTime()andJpaExpression localTime()
    返回当前日期、日期和时间,或数据库服务器的时间。

类似于JPA的CriteriaBuilder接口所定义的定义表达式的方法,你可以使用这些方法来定义你的查询的投影或WHERE子句。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.equal(game.get("playedOn"), cBuilder.localDate()));

List<ChessGame> games = em.createQuery(q).getResultList();

然后,Hibernate在生成的SQL语句中包含这些表达式。你的数据库会处理它们并返回结果。如果你要处理返回的值,并依赖于时区或其他本地化,这就很重要。在这些情况下,你需要确保你的Java应用程序和数据库使用相同的设置。

11:58:59,183 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.playedOn,c1_0.playerBlack_id,c1_0.playerWhite_id,c1_0.version from ChessGame c1_0 where c1_0.playedOn=current_date

额外的谓词

Hibernate还提供了一些额外的谓词,你可以用它们来定义你的WHERE子句。最有趣的是不同版本的ilikenotilike方法,它们提供了一种简单的方法来定义不区分大小写的LIKE或NOT LIKE表达式。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.where(cBuilder.ilike(player.get("firstName"), "%ikar%"));

List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));

16:32:13,147 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 where c1_0.firstName ilike ? escape ''
16:32:13,148 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [VARCHAR] - [%ikar%]
16:32:13,168 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]

如果你将一个关联建模java.util.Map,你可以使用方法isMapEmptyisMapNotEmptymapSize 来检查Map是否包含或有多少个元素。

排序

JPA的CriteriaBuilder使你可以按照一个或多个实体属性的升序或降序来获取结果集。此外,HibernateCriteriaBuilder 还能让你定义对空值的处理,并按表达式的结果排序,例如,数据库函数的结果。

除了JPA的CriteriaBuilder定义的ascdesc 方法外,HibernateCriteriaBuilder还定义了每个方法的第二个版本,接受一个布尔值作为第二个方法参数。这个布尔值定义了是否应首先返回空值。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.orderBy(cBuilder.asc(player.get("firstName"), true));

List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));

17:24:56,003 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 order by c1_0.firstName asc nulls first
17:24:56,017 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=2, firstName=Fabiano, lastName=Caruana]
17:24:56,017 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]
17:24:56,017 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=1, firstName=Magnus, lastName=Carlsen]
17:24:56,017 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=3, firstName=Richard, lastName=Rapport]

如果你想根据一个表达式定义一个更复杂的ORDER BY子句,你需要调用其中的一个排序 方法。它们使你能够提供你想对结果进行排序的表达式,如果你想以升序或降序获得结果,以及你想如何处理空值。

我在下面的代码片断中使用了这个方法,以球员名字长度的升序来获得查询结果。在这个例子中,定义对空值的处理并没有任何意义。但是如果你要按不同的表达式来排列你的查询结果,你可以提供第三个方法参数来定义对空值的处理。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.orderBy(cBuilder.sort(cBuilder.length(player.get("firstName")), SortOrder.ASCENDING));

List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));

08:15:10,477 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 order by character_length(c1_0.firstName) asc
08:15:10,493 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=1, firstName=Magnus, lastName=Carlsen]
08:15:10,493 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]
08:15:10,493 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=2, firstName=Fabiano, lastName=Caruana]
08:15:10,493 INFO  [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=3, firstName=Richard, lastName=Rapport]

设置操作

Hibernate 6还引入了对HQL和Criteria查询的集合操作的支持。使用HibernateCriteriaBuilder,你现在可以使用unionunionAllintersectintersectAllexceptexceptAll等方法将两个查询语句的结果集结合起来。

在这里你可以看到一个例子,在第一个查询中选择所有ChessPlayer的名字和姓氏,在第二个查询中选择所有ChessStreamer的名字和姓氏,并创建两个结果集的联合。当使用集合操作时,请记住,所有的结果集需要遵循相同的结构。

HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<Tuple> qPlayer = cBuilder.createTupleQuery();
Root<ChessPlayer> player = qPlayer.from(ChessPlayer.class);
qPlayer.multiselect(player.get("firstName").alias("firstName"), player.get("lastName").alias("lastName"));

CriteriaQuery<Tuple> qStreamer = cBuilder.createTupleQuery();
Root<ChessStreamer> streamer = qStreamer.from(ChessStreamer.class);
qStreamer.multiselect(streamer.get("firstName").alias("firstName"), streamer.get("lastName").alias("lastName"));

CriteriaQuery<Tuple> qPlayerAndStreamer = cBuilder.union(qPlayer, qStreamer);

List<Tuple> persons = em.createQuery(qPlayerAndStreamer).getResultList();
persons.forEach(t -> log.info(t.get("firstName") + ", " + t.get("lastName")));

正如你在日志输出中看到的,Hibernate生成了一条SQL语句,告诉数据库对包含所有ChessPlayerChessStreamer的首字母和尾字母的两个结果集应用集合操作union。

17:43:05,857 DEBUG [org.hibernate.SQL] - select c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 union select c2_0.firstName,c2_0.lastName from ChessStreamer c2_0
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Hikaru, Nakamura
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Fabiano, Caruana
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Magnus, Carlsen
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Richard, Rapport
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Levy, Rozman
17:43:05,865 INFO  [com.thorben.janssen.sample.TestSample] - Ben, Finegold

总结

正如你在本文中所看到的,Hibernate的HibernateCriteriaBuilder 接口扩展了JPA的CriteriaBuilder 接口,并为Hibernate的专有查询功能增加了方法。这些都是。

  • 额外的表达式,如roundexp,你可以用来执行计算,转换或提取信息,并获得当前的日期或时间。
  • 额外的谓词,如ilike谓词,你可以用来定义你的WHERE子句。
  • 定义更复杂的ORDER BY子句的方法,例如,基于SQL函数的结果。
  • 组合多个查询结果的集合操作。