EntityManager.createNativeQuery 使用分页时返回对象列表而不是 BigDecimal 列表

EntityManager.createNativeQuery returning list of objects instead of list of BigDecimal when using Pagination

我正在尝试将分页与 EntityManager.createNativeQuery() 结合使用。下面是我正在使用的框架代码:

var query = em.createNativeQuery("select distinct id from ... group by ... having ...");
List<BigDecimal> results = query
        .setMaxResults(pageSize)
        .setFirstResult(pageNumber * pageSize)
        .getResultList();

pageNumber 为 0(第一页)时,我得到预期的 BigDecimals 列表:

但是一旦 pageNumber > 0(示例,第二页),我就会得到一个对象列表,并且此列表中的每个对象似乎都包含两个 BigDecimals,其中第一个包含来自的值db,第二个BigDecimal好像是这一行的位置。

显然我遇到了这个异常

java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class java.math.BigDecimal

有人可以解释这个差异吗,以及如何将其固定为始终 return BigDecimals 列表?谢谢。

Update-1 :我创建了 a sample project 来重现这个问题。我只能使用 Oracle 数据库重现此问题。使用 H2 数据库,它运行良好,我始终得到一个与页码无关的 BigDecimals 列表。

Update-2 :我还创建了 a sample project with H2 并且没有这个问题。

我模拟了您的咨询,一切正常。我使用 DataJpaTest 为我实例化 entityManager,h2 内存数据库,JUnit 5 用于 运行 测试。见下文:

@Test
public void shouldGetListOfSalaryPaginated() {
    // given
    Person alex = new Person("alex");
    alex.setSalary(BigDecimal.valueOf(3305.33));
    Person john = new Person("john");
    john.setSalary(BigDecimal.valueOf(33054.10));
    Person ana = new Person("ana");
    ana.setSalary(BigDecimal.valueOf(1223));
    
    entityManager.persist(alex);
    entityManager.persist(john);
    entityManager.persist(ana);
    entityManager.flush();
    entityManager.clear();

    // when
    List<BigDecimal> found = entityManager.createNativeQuery("SELECT salary FROM person").setMaxResults(2).setFirstResult(2*1).getResultList();

    // then
    Assertions.assertEquals(found.size(), 1);
    Assertions.assertEquals(found.get(0).longValue(), 1223L);
}

我建议您查看您的本机查询。最好改用 Criteria API 并让本机查询针对复杂咨询等极端情况。

更新

作者贴出项目后,问题重现,与oracle方言有关。出于未知原因,第二次调用 运行ning 的查询是:select * from ( select row_.*, rownum rownum_ from ( SELECT c.SHOP_ID FROM CUSTOMER c ) row_ where rownum <= ?) where rownum_ > ?,这就是产生错误的原因,因为它查询 2 列而不是仅查询 1 列。不受欢迎的是这个rownum。其他方言就没有这个问题。

我建议您尝试其他 oracle 方言版本,看看它们 none 是否有效,我最后的建议是尝试自己进行分页。

问题的根本原因在于休眠中分页的实现方式 oracle dialect

有两种情况:

  1. 当我们有 setFirstResult(0) 时,将生成以下 sql:
-- setMaxResults(5).setFirstResult(0)
select * from (
  select test_id from TST_MY_TEST -- this is your initial query
) 
where rownum <= 5;

如您所见,此查询 returns 列列表与您的初始查询完全相同,因此您对这种情况没有问题。

  1. 当我们在非0值中设置setFirstResult时,将生成以下sql:
-- setMaxResults(5).setFirstResult(2)
select * from (
   select row_.*, rownum rownum_ 
   from (
      select test_id from TST_MY_TEST -- this is your initial query
   ) row_ 
   where rownum <= 5
) 
where rownum_ > 2

如您所见,此查询 returns 列列表包含额外的 rownum_ 列,因此您在将此结果集转换为 BigDecimal 时遇到问题。

解决方案

如果您使用 Oracle 12c R1 (12.1) 或更高版本,您可以使用新 row limiting clause 以这种方式在您的方言中覆盖此行为:

import org.hibernate.dialect.Oracle12cDialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.LimitHelper;
import org.hibernate.engine.spi.RowSelection;


public class MyOracleDialect extends Oracle12cDialect
{
   private static final AbstractLimitHandler LIMIT_HANDLER = new AbstractLimitHandler() {
      @Override
      public String processSql(String sql, RowSelection selection) {
         final boolean hasOffset = LimitHelper.hasFirstRow(selection);
         final StringBuilder pagingSelect = new StringBuilder(sql.length() + 50);
         pagingSelect.append(sql);
         
         /*
            see the documentation https://docs.oracle.com/database/121/SQLRF/statements_10002.htm#BABHFGAA
            (Restrictions on the row_limiting_clause)
            You cannot specify this clause with the for_update_clause.
          */
         if (hasOffset) {
            pagingSelect.append(" OFFSET ? ROWS");
         }
         pagingSelect.append(" FETCH NEXT ? ROWS ONLY");
         return pagingSelect.toString();
      }

      @Override
      public boolean supportsLimit() {
         return true;
      }
   };

   public MyOracleDialect()
   {
   }
   
   @Override
   public LimitHandler getLimitHandler() {
      return LIMIT_HANDLER;
   }
}

然后使用它。

<property name="hibernate.dialect">com.me.MyOracleDialect</property>

对于我的以下查询的测试数据集:

NativeQuery query = session.createNativeQuery(
   "select test_id from TST_MY_TEST"
).setMaxResults(5).setFirstResult(2);

List<BigDecimal> results = query.getResultList();

我得到了:

Hibernate: 
/* dynamic native SQL query */
select test_id  from TST_MY_TEST
OFFSET ? ROWS FETCH NEXT ? ROWS ONLY

val = 3
val = 4
val = 5
val = 6
val = 7

P.S。另见 HHH-12087

P.P.S 我通过删除 checking presents FOR UPDATE 子句简化了 AbstractLimitHandler 的实现。我认为在这种情况下,通过这种检查,我们不会有任何好处。

例如以下情况:

NativeQuery query = session.createNativeQuery(
   "select test_id from TST_MY_TEST FOR UPDATE OF test_id"
).setMaxResults(5).setFirstResult(2);

hibernate (with Oracle12cDialect) 将生成以下 sql:

/* dynamic native SQL query */
select * from (
  select
     row_.*,
     rownum rownum_ 
  from (
     select test_id from TST_MY_TEST -- initial sql without FOR UPDATE clause
  ) row_ 
  where rownum <= 5
) 
where rownum_ > 2
FOR UPDATE OF test_id -- moved for_update_clause

如您所见,hibernate 尝试通过将 FOR UPDATE 移动到查询的末尾来修复查询。但无论如何,我们会得到:

ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc.

在对不同 spring 库的不同版本进行了大量跟踪之后,我终于能够找出问题所在。在我的一次尝试中,当我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE 后,问题似乎就消失了。我查了一下spring-data-commons中的changelog of this release, and this bug, which is related to this bug,是这个问题的根本原因。我在升级 spring-data-commons 库后解决了这个问题。

您 运行 遇到的问题是您的 OracleDialect 向其选定的结果集中添加了一列。正如 SternK 的回答中所讨论的那样,它包装了您 运行 的查询。

如果您使用的是 Hibernate SessionFactory 和 Session 接口,那么您要寻找的函数就是“addScalar”方法。不幸的是,似乎没有纯 JPA 的实现(请参阅此处提出的问题:Does JPA have an equivalent to Hibernate SQLQuery.addScalar()?)。

我希望您当前的实现在 DB2、H2、HSQL、Postgres、MySQL(以及其他一些数据库引擎)中工作得很好。但是,在 Oracle 中,它向 ResultSet 添加了一个 row-number 列,这意味着 Hibernate 从 ResultSet 中获取了 2 列。在这种情况下,Hibernate 不执行任何查询解析,这意味着它只是将 ResultSet 解析到您的 List 中。由于它获得 2 个值,因此它将它们转换为 Object[] 而不是 BigDecimal.

作为一个警告,依靠 JDBC 驱动程序提供 expected-data-type 有点危险,因为 Hibernate 会询问 JDBC 驱动程序它建议哪个 data-type .在这种情况下,它建议使用 BigDecimal,但在某些条件下和某些实现将被允许 return Double 或其他一些类型。

那么你有几个选择。

  1. 您可以修改 oracle-dialect(如 SternK 所建议的)。这将利用备用 oracle-paging 实现。

  2. 如果您不反对在 JPA 实现中包含 hibnerate-specific 方面,那么您可以利用 JPA 标准中未提供的其他休眠功能。 (见以下代码...)

    List<BigDecimal> results = entitymanager.createNativeQuery("select distinct id from ... group by ... having ...")
            .unwrap(org.hibernate.query.NativeQuery.class)
            .addScalar("id", BigDecimalType.INSTANCE)
            .getResultList();
    System.out.println(results);
    

这确实有显式告诉 hibnerate 的优点,你只对 ResultSet 的“id”列感兴趣,而 hibernate 需要显式地将 returned 对象转换为 BigDecimal, JDBC-driver 是否应该决定另一种类型作为默认值更合适。