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

问题描述

我正在尝试对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:类[Ljava.lang.Object;不能转换为类java.math.BigDecimal

有人可以解释这个差异吗?如何解决这个差异以始终返回BigDecimals列表?谢谢。

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

Update-2 :我还创建了a sample project with H2,可以在没有此问题的情况下正常工作。

解决方法

您遇到的问题是OracleDialect将一列添加到其选定的ResultSet中。它包装了您正在运行的查询,如SternK的答案中所述。

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

我希望您当前的实现在DB2,H2,HSQL,Postgres,MySQL(和其他一些DB引擎)中都能正常工作。但是,在Oracle中,它将行号列添加到ResultSet中,这意味着Hibernate从ResultSet中获得2列。在这种情况下,Hibernate不执行任何查询解析,这意味着它只是将ResultSet解析到您的List中。由于它获得2个值,因此将它们转换为Object []而不是BigDecimal。

作为警告,依靠JDBC驱动程序提供期望的数据类型有点危险,因为Hibernate会询问JDBC驱动程序建议的数据类型。在这种情况下,它建议使用BigDecimal,但在某些条件和某些实现下,将允许返回Double或其他类型。

那么您有两个选择。

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

  2. 如果您不反对在JPA实现中使用特定于Hibrate的方面,则可以利用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”列感兴趣,并且如果JDBC驱动程序决定将hibernate显式转换为返回给BigDecimal的对象,默认情况下,更适合使用其他类型。

,

问题的根本原因在于分页在休眠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;

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

  1. 当我们将setFirstResult设置为非0值时,将生成以下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

如您所见,此查询返回带有其他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我通过删除检查提示AbstractLimitHandler子句简化了FOR UPDATE的实现。我认为在这种情况下以及进行这种检查不会有什么好处。

例如以下情况:

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

休眠(带有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.
,

我已经模拟了您的咨询,并且一切正常。我已经使用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方言有关。出于未知原因,正在为第二个调用运行的查询是:select * from ( select row_.*,rownum rownum_ from ( SELECT c.SHOP_ID FROM CUSTOMER c ) row_ where rownum <= ?) where rownum_ > ?,这就是生成错误的原因,因为它正在查询2列而不是仅查询1列。不需要的是这个rownum。对于其他方言,则没有这样的问题。

我建议您尝试使用其他oracle方言版本,以及它们是否均不起作用,我的最后提示是尝试自己进行分页。

,

在使用不同版本的不同spring库进行了很多研究之后,我终于能够找出问题所在。在我的尝试之一中,一旦我将spring-data-commons库从v2.1.5.RELEASE更新到v2.1.6.RELEASE,问题似乎就消失了。我查看了changelog of this release,在弹簧数据常见问题中与this bug相关的this bug是此问题的根本原因。升级spring-data-commons库后,我能够解决此问题。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...