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

huangapple go评论77阅读模式
英文:

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(第一页)时,我得到了预期的BigDecimal列表:

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

但是一旦pageNumber > 0(例如,第二页),我得到了一个对象列表,其中每个对象似乎包含两个BigDecimal值,第一个BigDecimal包含来自数据库的值,而第二个BigDecimal似乎是该行的位置。

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

显然我会得到以下异常:

java.lang.ClassCastException: 无法将类 [Ljava.lang.Object; 强制转换为类 java.math.BigDecimal

请问是否有人能够解释这种差异,并且如何修复以始终返回BigDecimal列表?谢谢。

更新-1:我创建了一个示例项目来复现这个问题。我只能在Oracle数据库中复现这个问题。在H2数据库中,它工作正常,而且无论页面编号如何,我始终都得到了BigDecimal列表。

更新-2:我还创建了一个使用H2的示例项目,在这个项目中,它可以正常工作,没有这个问题。

英文:

I am trying to use Pagination with EntityManager.createNativeQuery(). Below is the skeleton code that I am using:

var query = em.createNativeQuery(&quot;select distinct id from ... group by ... having ...&quot;);
List&lt;BigDecimal&gt; results = query
        .setMaxResults(pageSize)
        .setFirstResult(pageNumber * pageSize)
        .getResultList();

When pageNumber is 0 (first page), I get the expected List of BigDecimals:

<img src="https://i.stack.imgur.com/5VS5i.png" width="300" height="120">

But as soon as pageNumber > 0 (example, second page), I get a List of Objects, and each object in this list seems to contain two BigDecimals, the first of which contains the value from the db, and the second BigDecimal seems to be the position of this row.

<img src="https://i.stack.imgur.com/MoQrZ.png" width="300" height="190">

and obviously I get this exception

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

Can someone please explain this discrepancy, and how this can be fixed to always return a List of BigDecimals? Thank you.

Update-1 : I have created a sample project to reproduce this issue. I was able to reproduce this issue only with an Oracle database. With H2 database, it worked fine, and I consistently got a list of BigDecimals irrelevant of the page number.

Update-2 : I have also created a sample project with H2 where it works without this issue.

答案1

得分: 6

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

如果您正在使用Hibernate SessionFactory和Session接口,那么您要寻找的功能将是"addScalar"方法。不幸的是,在纯JPA中似乎没有实现(参见此处提出的问题:https://stackoverflow.com/questions/4227578/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. 您可以像SternK建议的那样修改您的oracle-dialect。这将利用备用的Oracle分页实现。
  2. 如果您不反对在您的JPA实现中具有Hibernate特定的方面,那么您可以利用JPA标准中未提供的其他Hibernate功能。(请参见以下代码...)
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驱动程序决定使用不同的类型作为默认类型的话。

英文:

The problem that you are running into is that your OracleDialect adds a column to its selected ResultSet. It wraps the query that you are running as discussed in SternK's answer.

If you were using the Hibernate SessionFactory and the Session interfaces, then the function that you would be looking for would be the "addScalar" method. Unfortunately, there doesn't seem to be an implementation in pure JPA (see the question asked here: https://stackoverflow.com/questions/4227578/does-jpa-have-an-equivalent-to-hibernate-sqlquery-addscalar).

I would expect your current implementation to work just fine in DB2, H2, HSQL, Postgres, MySQL (and a few other DB engines). However, in Oracle, it adds a row-number column to the ResultSet which means that Hibernate gets 2 columns from the ResultSet. Hibernate does not implement any query parsing in this case, which means that it simply parses the ResultSet into your List. Since it gets 2 values, it converts them into an Object[] rather than a BigDecimal.

As a caveat, relying on the JDBC driver to provide the expected-data-type is a bit dangerous, since Hibernate will ask the JDBC driver which data-type it suggests. In this case, it suggests a BigDecimal, but under certain conditions and certain implementations would be allowed to return a Double or some other type.

You have a couple options then.

  1. You can modify your oracle-dialect (as SternK) suggests. This will take advantage of an alternate oracle-paging implementation.

  2. If you are not opposed to having hibnerate-specific aspects in your JPA implementation, then you can take advantage of additional hibernate functions that are not offered in the JPA standard. (See the following code...)

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

This does have the advantage of explicitly telling hibnerate, that you are only interested in the "id" column of your ResultSet, and that hibernate needs to explicitly convert to the returned object to a BigDecimal, should the JDBC-driver decide that a different type would be more appropriate as a default.

答案2

得分: 4

你问题的根本原因在于分页在你的 Hibernate Oracle 方言 中的实现方式。

有两种情况:

  1. 当我们使用 setFirstResult(0) 时,将生成以下 SQL 查询:
-- setMaxResults(5).setFirstResult(0)
select * from (
  select test_id from TST_MY_TEST -- 这是您的初始查询
) 
where rownum &lt;= 5;

正如您所看到的,此查询返回与您的初始查询完全相同的列列表,因此在这种情况下不会出现问题。

  1. 当我们将 setFirstResult 设置为非 0 值时,将生成以下 SQL 查询:
-- setMaxResults(5).setFirstResult(2)
select * from (
   select row_.*, rownum rownum_ 
   from (
      select test_id from TST_MY_TEST -- 这是您的初始查询
   ) row_ 
   where rownum &lt;= 5
) 
where rownum_ &gt; 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);
         
         /*
            参见文档 https://docs.oracle.com/database/121/SQLRF/statements_10002.htm#BABHFGAA
            (对 row_limiting_clause 的限制)
            不能与 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: 
/* 动态本地 SQL 查询 */
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

再附注:我通过删除检查 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:

/* 动态本地 SQL 查询 */
select * from (
  select
     row_.*,
     rownum rownum_ 
  from (
     select test_id from TST_MY_TEST -- 初始没有 FOR UPDATE 子句的 SQL
  ) row_ 
  where rownum &lt;= 5
) 
where rownum_ &gt; 2
FOR UPDATE OF test_id -- 将 for_update_clause 移到了查询末尾

正如您所看到的,Hibernate 试图通过将 FOR UPDATE 移到查询末尾来修复查询。但无论如何,我们将会得到:

ORA-02014: 无法从带有 DISTINCT、GROUP BY 等的视图进行 FOR UPDATE 选择。
英文:

The root cause of your problem in the way how the pagination implemented in your hibernate oracle dialect.

There are two cases:

  1. When we have setFirstResult(0) the following sql will be generated:
-- setMaxResults(5).setFirstResult(0)
select * from (
  select test_id from TST_MY_TEST -- this is your initial query
) 
where rownum &lt;= 5;

As you can see, this query returns exactly the same columns list as your initial query, and therefore you do not have problem with this case.

  1. When we set setFirstResult in not 0 value the following sql will be generated:
-- 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 &lt;= 5
) 
where rownum_ &gt; 2

As you can see, this query returns the columns list with additional rownum_ column, and therefore you do have the problem with casting this result set to the BigDecimal.

Solution

If you use Oracle 12c R1 (12.1) or higher you can override this behavior in your dialect using new row limiting clause in this way:

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(&quot; OFFSET ? ROWS&quot;);
         }
         pagingSelect.append(&quot; FETCH NEXT ? ROWS ONLY&quot;);
         return pagingSelect.toString();
      }

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

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

and then use it.

&lt;property name=&quot;hibernate.dialect&quot;&gt;com.me.MyOracleDialect&lt;/property&gt;

For my test data set for the following query:

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

List&lt;BigDecimal&gt; results = query.getResultList();

I got:

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. See also HHH-12087

P.P.S I simplified my implementation of the AbstractLimitHandler by removing checking presents FOR UPDATE clause. I think we will not have nothing good in this case and with this checking.

For example for the following case:

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

hibernate (with Oracle12cDialect) will generate the following 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 &lt;= 5
) 
where rownum_ &gt; 2
FOR UPDATE OF test_id -- moved for_update_clause

As you can see, hibernate tries to fix query by moving FOR UPDATE to the end of the query. But anyway, we will get:

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

答案3

得分: 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 <= ?) where rownum_ > ?,这就是为什么会出现错误的原因,因为它查询了两列而不仅仅是一列。不需要的列就是 rownum。对于其他方言,没有这样的问题。

我建议您尝试其他版本的 Oracle 方言,如果没有一个起作用,我最后的建议是尝试自己实现分页。

英文:

I've simulated your consult and everything works fine. I've used DataJpaTest to instance entityManager for me, h2 memory database and JUnit 5 to run the test. See below:

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

    // when
    List&lt;BigDecimal&gt; found = entityManager.createNativeQuery(&quot;SELECT salary FROM person&quot;).setMaxResults(2).setFirstResult(2*1).getResultList();

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

I suggest that you review your native query. It's preferable that you use Criteria API instead and let native queries for extreme cases like complex consults.

Update

After the author posted the project, I could reproduce the problem and it was related to the oracle dialect. For unknown reason the query which is running for the second call is: select * from ( select row_.*, rownum rownum_ from ( SELECT c.SHOP_ID FROM CUSTOMER c ) row_ where rownum &lt;= ?) where rownum_ &gt; ?, and that's why this is generating a bug, because it's querying 2 columns instead of only one. The undesired one is this rownum. For other dialects there is no such problem.

I suggest you try other oracle dialect version and whether none of them work, my final tip is try to do the pagination yourself.

答案4

得分: 2

经过尝试不同版本的不同 Spring 库,我终于能够找出问题所在。在我的一次尝试中,问题似乎已经消失了,当我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE 时。我查阅了这个版本的更新日志,而与之相关的这个错误,是导致这个问题的根本原因。在我升级了 spring-data-commons 库之后,我成功解决了这个问题。

英文:

After a lot of trails with different versions of different spring libraries, I was finally able to figure out the issue. In one of my attempts, the issue seems to have disappeared, as soon as I updated the spring-data-commons library from v2.1.5.RELEASE to v2.1.6.RELEASE. I looked up the changelog of this release, and this bug, which is related to this bug in spring-data-commons, is the root cause of this issue. I was able to fix the issue after upgrading the spring-data-commons library.

huangapple
  • 本文由 发表于 2020年9月4日 17:56:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/63738889.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定