使用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
参数始终设置,可以是多个字段。
解决方案直到现在
到目前为止,我有一些解决方案:
- 一点也不好,但它解决了这个问题:
使用提供的查询,我只选择没有分页的id,只执行indexOf
以找到它的位置,并根据我可以找到记录页面的位置,然后使用getRecordsPage(long page, int pageSize)
执行常规过程getRecordsPage(long page, int pageSize)
已经实施。
- 不太好,因为与数据库高度耦合:
当我使用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)
。
好的解决方案
要求:
- 应支持多个领域的订单;
- 给定一个查询,它将返回记录位置或包含记录页面的偏移量;
一个好的解决方案是:
- 纯粹是JPA;
- 如果在数据库中执行一个额外的查询只是为了找出记录位置,那就好了;
- 如果在某些时候使用了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
类型具有您用于排序的属性foo
和bar
(即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 = :foo
和r.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
条件。)