将 Hibernate 查询表达为类型安全的 Java 流
使用 Criteria API 编写 Hibernate 查询可能会非常不直观,而且会显得冗长。在本文中,您将了解 Quarkus 扩展如何无需不必要的复杂性即可实现类型安全的 Hibernate 查询。
尽管 JPA Criteria Builder 富于表现力,但 JPA 查询通常同样冗长,而且 API 本身使用起来可能不直观,尤其是对于新手而言。在 Quarkus 生态系统中,Panache 是在使用 Hibernate ORM 时解决这些问题的一种部分方法。尽管如此,我发现在组合除最简单查询之外的任何查询时,我都要在 Panache 的辅助方法、预配置的枚举和原始字符串之间进行权衡。您可以说我只是缺乏经验和不耐烦,或者可以承认完美的 API 对每个人来说都是无缝易用的。因此,可以朝着这个方向进一步改善编写 JPA 查询的用户体验。
仍然存在的一个缺点是,原始字符串本质上不是类型安全的,这意味着我的 IDE 无法提供代码补全的帮助,最多只能祝我好运。从积极的方面来看,Quarkus 可以在一瞬间重新启动应用程序,从而对我的代码进行快速的裁决。而且,没有什么比我在第五次而不是第十次尝试就写出了一个有效的查询时所感受到的那种发自内心的喜悦和真正的惊喜更令人满足了……
考虑到这一点,我们构建了开源库 JPAStreamer,以使编写 Hibernate 查询的过程更加直观、耗时更少,同时保持现有代码库不变。它通过允许将查询表示为标准的 Java Stream 来实现此目标。执行时,JPAStreamer 会将 Stream 管道转换为 HQL 查询以进行高效执行,从而避免了具体化除相关结果之外的任何内容。
以一个例子来说明——在某个随机数据库中,存在一个名为 person 的表,在 Hibernate 应用程序中由以下标准 @Entity
表示
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "person_id", nullable = false, updatable = false)
private Integer actorId;
@Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
private String firstName;
@Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
private String lastName;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
// Getters for all fields will follow from here
}
要使用 JPAStreamer 和 Hibernate ORM 获取 ID 为 1 的 Person
,您只需要这样做
@ApplicationScoped
public class PersonRepository {
@PersistenceContext
EntityManagerFactory entityManagerFactory;
private final JPAStreamer jpaStreamer;
public PersonRepository (EntityManagerFactory entityManagerFactory) {
jpaStreamer = JPAStreamer.of(entityManagerFactory); (1)
}
@Override
public Optional<Person> getPersonById(int id) {
return this.jpaStreamer.from(Person.class) (2)
.filter(Person$.personId.equal(id)) (3)
.findAny();
}
}
1 | 只需一行即可初始化 JPAStreamer,底层 JPA 提供程序会处理数据库配置。 |
2 | Stream 源设置为 Person 表。 |
3 | filter 操作被视为 SQL WHERE 子句,条件是使用 JPAStreamer 的谓词(稍后会详细介绍)以类型安全的方式表示的。 |
尽管看起来 JPAStreamer 操作的是所有 Person
对象,但管道已优化为单个查询,在此情况下为
select
person0_.person_id as person_id1_0_,
person0_.first_name as first_na2_0_,
person0_.last_name as last_nam3_0_,
person0_.created_at as created_4_0_,
from
person person0_
where
person0_.person_id=1
因此,只有与搜索条件匹配的 Person
才会被具体化。
接下来,我们看一个更复杂的例子,在此例子中,我正在搜索名字以“A”结尾且姓氏以“B”开头的人。匹配项首先按名字排序,然后按姓氏排序。我进一步决定应用偏移量 5,排除前五个结果,并将总结果限制为 10。这是实现此任务的 Stream 管道
List<Person> list = jpaStreamer.stream(Person.class)
.filter(Person$.firstName.endsWith("A").and(Person$.lastName.startsWith("B"))) (1)
.sorted(Person$.firstName.comparator().thenComparing(Person$.lastName.comparator())) (2)
.skip(5) (3)
.limit(10) (4)
.collect(Collectors.toList())
1 | 过滤器可以与 and/or 操作符结合使用 |
2 | 轻松过滤一个或多个属性 |
3 | 跳过前 5 个 Person |
4 | 最多返回 10 个 Person |
在查询方面,Stream 操作符 filter、sort、limit 和 skip 都具有自然的映射关系,使得生成的查询表达清晰、直观且紧凑。
上述 Stream 由 JPAStreamer 转换为以下 HQL 语句
select
person0_.person_id as person_id1_0_,
person0_.first_name as first_na2_0_,
person0_.last_name as last_nam3_0_,
person0_.created_at as created_4_0_,
from
person person0_
where
person0_.person_id=1
where
(person0_.first_name like ?)
and (person0_.last_name like ?)
order by
person0_.first_name asc,
person0_.last_name asc limit ?, ?
JPAStreamer 的工作原理
好的,它看起来很简单。但它是如何工作的呢?JPAStreamer 在编译时使用注解处理器来形成元模型。它会检查所有被标记为标准 JPA 注解 @Entity
的类,并为每个实体 Foo.class
,创建一个对应的 Foo$.class
。生成的类将实体属性表示为字段,用于形成 User$.firstName.startsWith("A")
形式的谓词,这些谓词可以被 JPAStreamer 的查询优化器解释。
值得再次强调的是,JPAStreamer 不会更改或干扰现有代码库,而只是扩展 API 以处理 Java Stream 查询。
安装 JPAstreamer 扩展
JPAStreamer 与任何其他 Quarkus 扩展一样安装,使用 Maven 依赖项
<dependency>
<groupId>io.quarkiverse.jpastreamer</groupId>
<artifactId>quarkus-jpastreamer</artifactId>
<version>1.0.0</version>
</dependency>
添加依赖项后,重新构建您的 Quarkus 应用程序以触发 JPAStreamer 的注解处理器。一旦生成的字段位于 /target/generated-sources
中,安装就完成了;您可以通过类名后面的 $ 符号来识别它们,例如 Person$.class
。
JPAStreamer 需要底层的 JPA 提供程序,例如 Hibernate ORM。因此,JPAStreamer 不需要额外的配置,因为数据库集成由 JPA 提供程序负责。 |
JPAStreamer 与 Panache
任何 Panache 的爱好者都会注意到 JPAStreamer 与 Panache 在简化许多常见查询方面有一些共同的目标。尽管如此,JPAStreamer 通过其类型安全的 Stream 接口来增强查询的信心,从而使自己与众不同。幸运的是,没有人被迫做出选择,因为 Panache 和 JPAStreamer 可以无缝地协同工作。
此处 提供了一个同时使用 JPAStreamer 和 Panache 的 Quarkus 应用程序示例。 |
在撰写本文时,JPAStreamer 尚不支持 Panache 的 Active Record Pattern,因为它依赖于标准的 JPA 实体来生成其元模型。这很可能会在不久的将来发生变化。
总结
JPA 总体上,特别是 Hibernate ORM,极大地简化了应用程序数据库访问,但其 API 有时会带来不必要的复杂性。通过 JPAstreamer,您可以使用 JPA,同时保持代码库的整洁和可维护。
资源
-
主页: jpastreamer.org
-
JPAStreamer Quarkus 演示: github.com/speedment/jpa-streamer-demo/tree/master/quarkus-hibernate-panache
-
Gitter 支持聊天: gitter.im/jpa-streamer