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

当是 0(第一页)时,我得到预期的大数点列表:pageNumber

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

显然我得到了这个例外

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

有人可以解释这种差异,以及如何解决这个问题以始终返回大数点列表吗?谢谢。

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

更新-2 :我还使用H2创建了一个示例项目,它可以正常工作而不会出现此问题。


答案 1

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

如果您使用的是Hibernate SessionFactory和Session接口,那么您要查找的功能将是“addScalar”方法。不幸的是,在纯JPA中似乎没有实现(请参阅这里提出的问题:JPA是否具有与Hibernate SQLQuery.addScalar()等效的实现?

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

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

那么你有几个选择。

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

  2. 如果您不反对在 JPA 实现中具有特定于 hibnerate 的方面,那么您可以利用 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 驱动程序决定将不同类型的类型作为默认值更合适,则休眠需要显式转换为返回的对象到 BigDecimal。


答案 2

问题的根本原因在于分页如何在休眠 oracle 方言中实现。

有两种情况:

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

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

  1. 当我们设置 not 值时,将生成以下 sql:setFirstResult0
-- 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

附言:另见HHH-12087

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

例如,对于以下情况:

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

休眠 (with ) 将生成以下 sql:Oracle12cDialect

/* 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

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

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