【问题标题】:EntityManager.createNativeQuery returning list of objects instead of list of BigDecimal when using PaginationEntityManager.createNativeQuery 在使用分页时返回对象列表而不是 BigDecimal 列表
【发布时间】:2020-12-23 14:12:22
【问题描述】:

我正在尝试将分页与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(第一页)时,我得到了预期的 BigDecimal 列表:

但是只要pageNumber > 0(例如,第二页),我就会得到一个对象列表,这个列表中的每个对象似乎都包含两个 BigDecimal,其中第一个包含来自db,第二个 BigDecimal 似乎是这一行的位置。

显然我得到了这个异常

java.lang.ClassCastException: 类 [Ljava.lang.Object;不能转换为 java.math.BigDecimal 类

谁能解释一下这种差异,以及如何解决这个问题以始终返回 BigDecimals 列表?谢谢。

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

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

【问题讨论】:

  • 你用的是什么休眠版本?
  • @SternK Hibernate 版本 5.2.18
  • 尝试改用 BigInteger
  • @GuilhermeAlencar 不幸的是,将List&lt;BigDecimal&gt; 更改为List&lt;BigInteger&gt; results 并没有改变任何东西。奇怪的是,result 对象仍然具有与上面第二个屏幕截图相同的 BigDecimals。
  • 我的建议是在分配给您的列表之前调试 getResultList 的结果。查看问题是解组到您的对象还是咨询本身。

标签: java oracle hibernate pagination entitymanager


【解决方案1】:

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

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

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

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

那么你有几个选择。

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

  2. 如果您不反对在 JPA 实现中使用特定于休眠的方面,那么您可以利用 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);
    

这确实具有显式告诉休眠的优点,即您只对 ResultSet 的“id”列感兴趣,并且休眠需要将返回的对象显式转换为 BigDecimal,如果 JDBC 驱动程序决定不同的类型会更适合作为默认值。

【讨论】:

    【解决方案2】:

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

    附:另见HHH-12087

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

    例如以下情况:

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

    hibernate(使用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.
    

    【讨论】:

      【解决方案3】:

      我已经模拟了您的咨询,一切正常。我使用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 &lt;= ?) where rownum_ &gt; ?,这就是产生错误的原因,因为它查询的是 2 列而不是仅 1 列。不受欢迎的是这个rownum。其他方言则没有这样的问题。

      我建议你尝试其他oracle方言版本,无论它们都不起作用,我最后的提示是尝试自己进行分页。

      【讨论】:

      • 我使用本机查询的原因是因为我必须执行一个复杂的查询。查询在第一页返回一种类型的结果,而从第二页返回另一种类型的结果的行为非常令人费解,尤其是如果这种行为没有记录在案,并且不能用其他查询重现。
      • 我已经重现了与您公开的完全相同的行为(通过实体管理器本地查询大十进制分页列表)。它适用于第一页或第二页。我告诉您问题出在您的查询或存储的数据中,如果您希望我或任何人提供更多信息,您应该提供更多信息。对我来说就是这样,祝你好运!
      • 我最后的猜测是,你的列数据库的规模可能与 hibernate 的 bigdecimal 映射不匹配,这可能是造成这种混乱的原因。
      • 感谢您的努力和支持。不幸的是,到目前为止似乎没有任何帮助。我创建了一个示例项目来重现此问题。你可以在这里找到它:github.com/srikanth-lingala/…
      • 这里的问题是数据库方言。使用 H2(或 hsql、mysql、db2 或 postgres)无法重现此问题 - 它是 oracle 的功能。使用任何其他方言或数据库进行测试将适得其反,并导致错误的结论和讨论,因为最初问题中提供的代码几乎适用于所有数据库风格,除了 oracle(这是他在这里使用的)。
      【解决方案4】:

      在使用不同版本的不同 Spring 库进行了大量跟踪之后,我终于能够找出问题所在。在我的一次尝试中,一旦我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE,问题似乎就消失了。我查了changelog of this release,和spring-data-commons中与this bug相关的this bug,是这个问题的根源。升级 spring-data-commons 库后,我能够解决此问题。

      【讨论】:

      • 在这个新的 Spring Data 版本中你有什么休眠版本?你到底用的是什么hibernate方言?
      • 休眠版5.2.18.Final,休眠方言org.hibernate.dialect.Oracle10gDialect
      • 恐怕这个问题还在你身边。实际上,您在CustomerRepository 中的查询与此问题无关。您可以将其简化为@Query(value = "SELECT SHOP_ID FROM CUSTOMER", nativeQuery = true) Page&lt;BigDecimal&gt; findContractsToBeDeleted(Pageable pageable);
      • 唯一重要的是:您的查询返回一列,但由于与分页相关的查询修改,这一事实发生了变化,并返回了两列,如我的回答中所述。
      • 这就是问题所在。人们很难相信 oracle 方言实际上会导致除了所有选择的列之外还返回一个额外的列。
      猜你喜欢
      • 2020-07-17
      • 2012-06-23
      • 2018-12-18
      • 1970-01-01
      • 1970-01-01
      • 2021-07-05
      • 1970-01-01
      • 1970-01-01
      • 2021-09-18
      相关资源
      最近更新 更多