雅加达数据是Java持久化的一种新规范,计划作为EE 11平台的一部分发布。在上一篇文章中,我介绍了雅加达数据存储库的基本功能,重点介绍了雅加达数据如何提供编译时类型安全,使注释处理器能够执行静态分析。
这涉及到将以前用过程代码表示的一些信息移动到
- 
如 @Query和@Find这样的注解中,以及
- 
存储库方法参数的名称和类型。 
今天我们将讨论雅加达数据的一些更动态的功能。你可能预计这会导致类型安全性的损失,但我们找到了避免这种情况的方法。关键成分是一个静态元模型。
静态元模型
你可能已经熟悉雅加达持久化的静态元模型。这是我在JPA 2.0时期想出的一个主意,当时注解处理刚刚崭露头角;目标是将类型安全性引入JPA Criteria查询API。
对于一个实体类Book,注解处理器生成一个名为Book_的类,该类公开代表Book的持久字段的对象。例如,Book_.title代表实体Book的字段title。这在某种程度上只是针对我们等待超过四分之一个世纪才能在Java语言中获得方法和字段字面量的一个权宜之计。
雅加达数据引入了自己的静态元模型,它与雅加达持久化元模型不同,但概念上非常相似。与Book_不同,雅加达数据为Book的静态元模型由类_Book公开。
让我们通过考虑一个简单的例子来看看静态元模型是如何有用的。
我们即将遇到一个名为Sort的类,它代表基于实体字段的排序。通过传递字段名可以很容易地获取Sort的实例
var sort = Sort.asc("title");遗憾的是,由于这是常规过程代码,而不是注解,因此无法在编译时验证字段名"title"。所以这是一种不好的做法。
一个更好的解决方案是使用静态元模型来获取一个Sort的实例。
var sort = _Book.title.asc();静态元模型还声明了包含持久字段名称的常量。例如,_Book.TITLE评估为字符串"title"。这些常量非常有用,因为它们可以用作注解值。
按静态顺序排序
一个@Find或@Query方法可以指定查询结果的顺序。当然,实现这一点的其中一种方法就是使用JDQL/JPQL的order by子句。
@Query("where title like ?1 and yearPublished = ?2 " +
       "order by title, isbn")
List<Book> booksByTitle(String title, Year yearPublished);请回忆一下之前的文章,HibernateProcessor会在编译时验证JDQL查询。如果Book没有名为title的字段,你会立即知道。
一种替代方法,特别是对于@Find方法非常有用,就是使用@OrderBy注解
@Find
@OrderBy("title")
@OrderBy("isbn")
List<Book> booksByTitle(String title, Year yearPublished);这仍然是完全类型安全的!HibernateProcessor会在编译时检查Book是否有名为title和isbn的字段。如果你对此难以接受,这里有一个完全相同类型安全的替代方法
@Find
@OrderBy(_Book.TITLE)
@OrderBy(_Book.ISBN)
List<Book> booksByTitle(String title);排序通常涉及动态选择实体字段,在这种情况下,我们刚刚看到的任何解决方案都不适用。相反,我们需要一种方法来将表示排序标准的对象传递给仓库方法。
分页和动态排序
查询方法可以具有额外的参数,这些参数指定
- 
附加的排序标准,以及/或 
- 
限制和偏移量,以限制返回给客户端的结果。 
动态排序
动态排序标准使用Sort和Order类型表示
- 
Sort的一个实例表示对查询结果排序的单个标准,
- 
而 Order的一个实例将多个Sort组合在一起。
查询方法可以接受一个Sort的实例。
@Find
List<Book> books(@Pattern String title, Year yearPublished,
                 Sort<Book> sort);这个方法可以这样调用
var books =
        library.books(pattern, year,
                      _Book.title.ascIgnoreCase());或者方法可以接受一个Order的实例。
@Find
List<Book> books(@Pattern String title, Year yearPublished,
                 Order<Book> order);方法现在可以这样调用
var books =
       library.books(pattern, year,
                     Order.of(_Book.title.ascIgnoreCase(),
                              _Book.isbn.asc());动态排序标准可以与静态标准结合。
@Find
@OrderBy("title")
List<Book> books(@Pattern String title, Year yearPublished,
                 Sort<Book> sort);我们并不认为这在实际中有很大的用处。
限制
Limit是表达查询结果子范围的最简单方法。它指定了
- 
maxResults,从数据库服务器返回给客户端的最大结果数,
- 
以及可选的, startAt,从第一个结果开始的偏移量。
这些值直接映射到Jakarta Persistence Query接口的熟悉的setMaxResults()和setFirstResults()。
@Find
@OrderBy(_Book.TITLE)
List<Book> books(@Pattern String title, Year yearPublished,
                 Limit limit);var books =
        library.books(pattern, year,
                      Limit.of(MAX_RESULTS));PageRequest提供了一种更复杂的方法。
基于偏移量的分页
PageRequest表面上与Limit相似,但它是以
- 
页面 size和
- 
页码 page来指定的。
我们可以像使用Limit一样使用PageRequest。
@Find
@OrderBy("title")
@OrderBy("isbn")
List<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest);var books =
        library.books(pattern, year,
                      PageRequest.ofSize(PAGE_SIZE));当仓库方法用于分页时,查询结果应该是完全有序的。确保你有良好的总排序的最简单方法是指定实体的标识符作为排序的最后一个元素。这就是为什么我们在前面的例子中指定了@OrderBy("isbn")。
接受PageRequest的仓库方法可以返回一个Page而不是一个List,这使得实现分页更容易。
@Find
@OrderBy("title")
@OrderBy("isbn")
Page<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest);var page =
        library.books(pattern, year,
                      PageRequest.ofSize(PAGE_SIZE));
var books = page.content();
long totalPages = page.totalPages();
// ...
while (page.hasNext()) {
    page = library.books(pattern, year,
                         page.nextPageRequest().withoutTotal());
    books = page.content();
    // ...
}分页可以与动态排序结合。
@Find
Page<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest, Order<Book> order);返回类型为Page的仓库方法使用SQL中的offset和limit(或类似功能,取决于数据库)来实现分页。这被称为基于偏移量的分页。基于偏移量的分页的一个问题是,当数据库在页面请求之间被修改时,很容易出现遗漏或重复的结果。因此,Jakarta Data提供了一个替代方案,我更喜欢将其称为基于键的分页。
| 规范与我不同,将其称为基于游标的分页,但请不要将其与数据库级别的游标混淆。有时也称为"keyset"分页,但这个术语几乎没有什么意义,因为只涉及一个键。同样,基于偏移量的分页有时也称为"rowset"分页。这甚至更糟:一组行按定义是一个关系,即一个表。 | 
基于键的分页
在基于键的分页中,查询结果必须按结果集的唯一键完全排序。SQL中的offset被查询的where子句中唯一键的限制所取代
- 
请求查询结果的下一页使用当前页上的最后一个结果的键值来限制结果,或者 
- 
请求查询结果的前一页使用当前页上的第一个结果的键值来限制结果。 
对于基于键的分页,查询有一个完整排序是至关重要的。
从Jakarta Data用户的视角来看,基于键的分页几乎与基于偏移量的分页完全一样。区别在于我们必须声明我们的仓库方法返回CursoredPage。
@Find
@OrderBy("title")
@OrderBy("isbn")
CursoredPage<Book> books(@Pattern String title, Year yearPublished,
                         PageRequest pageRequest);另一方面,在基于键的分页中,Hibernate必须在幕后做一些工作来重写我们的查询。
当前状态
我们离完成Jakarta Data 1.0最终提案的工作仅剩几天,尽管规范仍需进行审查投票。
Hibernate数据仓库已经完全实现了该规范,但当前的计划是将它作为Hibernate 6.6的一部分发布。它已经在Hibernate 6.5 CR1中以某种形式的"预览"形式提供,但我们需要对StatelessSession进行一些重要的增强,我们认为将这些内容偷偷加入CR2版本并不完全合适。
更新:Hibernate数据仓库现在通过了Jakarta Data TCK测试。