8

如何知道 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. 一点都不好,但它解决了问题:

使用提供的查询,我只选择不分页的 id 并只执行 aindexOf以找到它的位置,并根据位置我可以找出记录所在的页面,然后使用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. 如果在某些时候使用了休眠就可以了(因为在这种情况下休眠落后于 JPA)。
4

2 回答 2

6

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

于 2012-05-06T12:00:46.863 回答
2

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

您不需要本机查询来查找记录的位置,只需一个精心制作的SELECT COUNT(r).

为了使其具体化,让我们假设该Record类型具有属性foo并且bar您正在使用它来进行排序(即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 = :fooAND 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))。)

然后在您的所有排序列中都可能存在“关系”。如果发生这种情况,那么您的寻呼机函数getRecordsPage(long,int)

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

于 2012-07-27T03:02:50.147 回答