繁体   English   中英

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(例如,第二页),我就会得到一个对象列表,这个列表中的每个对象似乎都包含两个 BigDecimal,其中第一个包含来自数据库的值,第二个 BigDecimal 似乎包含成为这一行的位置。

显然我得到了这个例外

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

有人可以解释这种差异,以及如何修复它以始终返回 BigDecimals 列表? 谢谢你。

更新 1 :我创建了一个示例项目来重现此问题。 我只能使用 Oracle 数据库重现此问题。 使用 H2 数据库,它运行良好,并且我始终得到一个与页码无关的 BigDecimals 列表。

更新 2 :我还使用 H2创建了一个示例项目,它可以在没有此问题的情况下工作。

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

如果您正在使用 Hibernate SessionFactory 和 Session 接口,那么您要寻找的函数将是“addScalar”方法。 不幸的是,在纯 JPA 中似乎没有实现(请参阅此处提出的问题: Does JPA has 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-paging 实现。

  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);

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

您的问题的根本原因在于分页如何在您的 hibernate oracle 方言中实现

有两种情况:

  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) 或更高版本,您可以通过这种方式使用新的行限制子句在您的方言中覆盖此行为:

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

PS 另见HHH-12087

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

例如对于以下情况:

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 列而不是仅一列。 不受欢迎的是这个rownum 对于其他方言,没有这样的问题。

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

经过大量不同版本的不同 spring 库的跟踪后,我终于能够找出问题所在。 在我的一次尝试中,一旦我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE,问题似乎就消失了。 我查看了这个版本的更新日志这个 bug与 spring-data-commons 中的这个 bug相关,是这个问题的根本原因。 升级 spring-data-commons 库后,我能够解决这个问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM