英文:
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
列表:
但是一旦pageNumber
> 0(例如,第二页),我得到了一个对象列表,其中每个对象似乎包含两个BigDecimal
值,第一个BigDecimal
包含来自数据库的值,而第二个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("select distinct id from ... group by ... having ...");
List<BigDecimal> 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或其他某种类型。
然后您有几个选择。
- 您可以像SternK建议的那样修改您的oracle-dialect。这将利用备用的Oracle分页实现。
- 如果您不反对在您的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.
-
You can modify your oracle-dialect (as SternK) suggests. This will take advantage of an alternate oracle-paging implementation.
-
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<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);
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 方言 中的实现方式。
有两种情况:
- 当我们使用
setFirstResult(0)
时,将生成以下 SQL 查询:
-- setMaxResults(5).setFirstResult(0)
select * from (
select test_id from TST_MY_TEST -- 这是您的初始查询
)
where rownum <= 5;
正如您所看到的,此查询返回与您的初始查询完全相同的列列表,因此在这种情况下不会出现问题。
- 当我们将
setFirstResult
设置为非0
值时,将生成以下 SQL 查询:
-- setMaxResults(5).setFirstResult(2)
select * from (
select row_.*, rownum rownum_
from (
select test_id from TST_MY_TEST -- 这是您的初始查询
) 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);
/*
参见文档 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 <= 5
)
where rownum_ > 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:
- 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 <= 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.
- When we set
setFirstResult
in not0
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 <= 5
)
where rownum_ > 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(" 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;
}
}
and then use it.
<property name="hibernate.dialect">com.me.MyOracleDialect</property>
For my test data set for the following query:
NativeQuery query = session.createNativeQuery(
"select test_id from TST_MY_TEST"
).setMaxResults(5).setFirstResult(2);
List<BigDecimal> 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(
"select test_id from TST_MY_TEST FOR UPDATE OF test_id"
).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 <= 5
)
where rownum_ > 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("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);
}
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 <= ?) where rownum_ > ?
, 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论