使用JPA(Hibernate)查找包含给定记录的页面

如何知道JPA查询中记录的位置?

我有一个服务返回分页结果,或多或少实现了一个带有此签名的方法:

List getRecordsPage(long page, int pageSize); 

调用它时,我只需创建一个查询并进行如下配置:

 TypedQuery query = entityManager.createQuery(criteriaQuery); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); 

这会将结果分页。 这是按预期工作的,非常简单。

问题

我的另一个要求是实现一个方法来检索包含特定记录的页面。 使用以下签名实现方法:

 List getRecordsPage(Record record, int pageSize); 

此方法需要生成记录所在的右页。 例如,对于getRecordsPage(RECORD4, 2)调用,考虑数据库状态:

 1. RECORD1 2. RECORD2 3. RECORD3 4. RECORD4 5. RECORD5 

返回的页面应为2,包含[RECORD3, RECORD4]

ORDER BY参数始终设置,可以是多个字段。

解决方案直到现在

到目前为止,我有一些解决方案:

  1. 一点也不好,但它解决了这个问题:

使用提供的查询,我只选择没有分页的id,只执行indexOf以找到它的位置,并根据我可以找到记录页面的位置,然后使用getRecordsPage(long page, int pageSize)执行常规过程getRecordsPage(long page, int pageSize)已经实施。

  1. 不太好,因为与数据库高度耦合:

当我使用mySQL时,我可以执行一个sql: select r from (select rownum r, id from t order by x,y) z where z.id = :id ,什么会返回记录的位置而我可以使用它来调用getRecordsPage(long page, int pageSize)

好的解决方案

要求:

  1. 支持多个领域的订单;
  2. 给定一个查询,它将返回记录位置或包含记录页面的偏移量;

一个好的解决方案是:

  1. 纯粹是JPA;
  2. 如果在数据库中执行一个额外的查询只是为了找出记录位置,那就了;
  3. 如果在某些时候使用了hibernate,那就了(因为在这种情况下Hibernate落后于JPA)。

为了确保我理解正确:您正在显示一个Record并希望显示所有记录的分页列表,预先选择包含您的项目的页面?

首先,您必须知道关系数据库不提供数据库中任何记录的隐式排序。 虽然它们似乎从头到尾排序,但这不便携且可靠。

因此,您的分页列表/网格必须由某些列明确排序。 为简单起见,您的网格按id排序。 您知道当前显示的记录的ID(例如: X )。 您首先需要弄清楚您的记录中关于此排序顺序的记录位置:

 SELECT COUNT(r) FROM Record r WHERE r.id < :X 

此查询将返回记录的记录数。 现在很简单:

 int page = count / pageSize 

page是从0开始的。

不幸的是,如果您的排序列不是唯一的,这可能并不适用于所有情况。 但是如果列不是唯一的,则排序本身不稳定(具有相同值的记录可能以随机顺序出现),因此请考虑使用额外的唯一列进行排序:

 ... ORDER BY r.sex, r.id 

在这种情况下,记录首先按sex (大量重复)和id排序。 在当前记录之前计数记录的解决方案仍然有效。

鉴于赏金仍未分配,我将添加另一个答案。 (这与Tomasz Nurkiewicz的答案非常相似,但更多地讨论了我认为棘手的部分:使WHERE子句正确。如果它被认为是不礼貌,我会删除它。)

您不需要本机查询来查找记录的位置,只需要精心设计的SELECT COUNT(r)

为了使其具体化,让我们假设Record类型具有您用于排序的属性foobar (即ORDER BY foo, bar )。 然后你想要的方法的快速和脏版本看起来像这样(未经测试):

 List getRecordsPage(Record record, int pageSize) { // Note this is NOT a native query Query query = entityManager.createQuery( "SELECT COUNT(r) " + "FROM Record r " + "WHERE (r.foo < :foo) OR ((r.foo = :foo) AND (r.bar < :bar))"); query.setParameter("foo", record.getFoo()); query.setParameter("bar", record.getBar()); int zeroBasedPosition = ((Number) query.getSingleResult()).intValue(); int pageIndex = zeroBasedPosition / pageSize; return getRecordsPage(pageIndex, pageSize); } 

有两个棘手的考虑因素:完全正确地获取WHERE子句,并在所有排序列中处理“tie”。

关于WHERE子句,目标是计算排序“低于”给定Record 。 使用一个排序列很容易:它只是r.foo < :foo的记录,例如。

对于两个排序列,它稍微更难,因为第一列中可能存在“关系”,必须用第二列打破。 所以“较低”的记录要么有r.foo < :foo ,要么r.foo = :foor.bar < :bar 。 (如果有三个,那么你需要三个OR条件 - 类似于(r.foo < :foo) OR ((r.foo = :foo) AND (r.bar < :bar)) OR ((r.foo = :foo) AND (r.bar = :bar) AND (r.baz < :baz)) 。)

然后在所有排序列中都存在“联系”的可能性。 如果发生这种情况,那么你的寻呼机function( getRecordsPage(long,int) )每次都可能获得不同的结果(如果它被天真地实现)(因为数据库每次执行查询时都有不同顺序返回“相等”行的余地) 。

解决此问题的一种简单方法是按记录ID添加排序,以确保每次都以相同的方式断开关系。 并且你想要在你的问题中提到的函数中同时放置相应的逻辑。 这意味着在查询结束时添加id ,例如在pager函数中使用ORDER BY foo,bar,id ,并在getRecordsPage(Record,int)WHERE添加另一个OR条件。)