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

时间:2012-05-06 11:50:20

标签: java hibernate jpa criteria

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

我有一个服务可以返回分页结果,或者使用此签名实现或多或少的方法:

List<Record> getRecordsPage(long page, int pageSize);

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

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

此页面显示结果。这是按预期工作的,非常简单。

问题

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

List<Record> getRecordsPage(Record record, int pageSize);

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

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

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

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

直到现在的解决方案

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

  1. 一点也不好,但它解决了问题:
  2. 使用提供的查询我只选择没有分页的id,只执行indexOf以找到它的位置,并根据我可以找到记录页面的位置,然后使用getRecordsPage(long page, int pageSize)已经实施。

    1. 不太好,因为数据库很高:
    2. 当我使用mySQL时,我可以执行一个类似:select r from (select rownum r, id from t order by x,y) z where z.id = :id的sql,什么会返回记录的位置,我可以使用它来调用getRecordsPage(long page, int pageSize)

      良好解决方案

      要求:

      1. 支持多个字段的订单;
      2. 给出一个查询,它将返回记录位置或包含记录页面的偏移量;
      3. 一个好的解决方案是:

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

2 个答案:

答案 0 :(得分:5)

为了确保我理解正确:您正在显示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排序。在当前记录之前计算记录的解决方案仍然有效。

答案 1 :(得分:1)

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

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

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

List<Record> 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子句正确,并在所有排序列中处理“关系”。

关于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))。)

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

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