问题描述
我正在尝试对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(示例,第二页),我就会得到一个对象列表,并且该列表中的每个对象似乎都包含两个BigDecimals,其中第一个包含来自db的值,第二个BigDecimal似乎是该行的位置。
显然我得到了这个例外
java.lang.ClassCastException:类[Ljava.lang.Object;不能转换为类java.math.BigDecimal
有人可以解释这个差异吗?如何解决这个差异以始终返回BigDecimals列表?谢谢。
Update-1 :我创建了a sample project来重现此问题。我只能使用Oracle数据库来重现此问题。使用H2数据库,它运行良好,并且我始终获得与页码无关的BigDecimals列表。
Update-2 :我还创建了a sample project with H2,可以在没有此问题的情况下正常工作。
解决方法
您遇到的问题是OracleDialect将一列添加到其选定的ResultSet中。它包装了您正在运行的查询,如SternK的答案中所述。
如果您使用的是Hibernate SessionFactory和Session接口,那么您要查找的函数将是“ addScalar”方法。不幸的是,似乎没有纯JPA的实现(请参阅此处的问题:Does JPA have an equivalent to Hibernate SQLQuery.addScalar()?)。
我希望您当前的实现在DB2,H2,HSQL,Postgres,MySQL(和其他一些DB引擎)中都能正常工作。但是,在Oracle中,它将行号列添加到ResultSet中,这意味着Hibernate从ResultSet中获得2列。在这种情况下,Hibernate不执行任何查询解析,这意味着它只是将ResultSet解析到您的List中。由于它获得2个值,因此将它们转换为Object []而不是BigDecimal。
作为警告,依靠JDBC驱动程序提供期望的数据类型有点危险,因为Hibernate会询问JDBC驱动程序建议的数据类型。在这种情况下,它建议使用BigDecimal,但在某些条件和某些实现下,将允许返回Double或其他类型。
那么您有两个选择。
-
您可以修改oracle-方言(如SternK所建议)。这将利用备用的oracle分页实现。
-
如果您不反对在JPA实现中使用特定于Hibrate的方面,则可以利用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驱动程序决定将hibernate显式转换为返回给BigDecimal的对象,默认情况下,更适合使用其他类型。
,问题的根本原因在于分页在休眠oracle dialect中的实现方式。
有两种情况:
- 当我们拥有
setFirstResult(0)
时,将生成以下sql:
-- setMaxResults(5).setFirstResult(0)
select * from (
select test_id from TST_MY_TEST -- this is your initial query
)
where rownum <= 5;
如您所见,该查询返回的列列表与初始查询完全相同,因此在这种情况下您没有问题。
- 当我们将
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
P.S。另请参见HHH-12087
P.P.S我通过删除检查提示AbstractLimitHandler
子句简化了FOR UPDATE
的实现。我认为在这种情况下以及进行这种检查不会有什么好处。
例如以下情况:
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列而不是仅查询1列。不需要的是这个rownum
。对于其他方言,则没有这样的问题。
我建议您尝试使用其他oracle方言版本,以及它们是否均不起作用,我的最后提示是尝试自己进行分页。
,在使用不同版本的不同spring库进行了很多研究之后,我终于能够找出问题所在。在我的尝试之一中,一旦我将spring-data-commons库从v2.1.5.RELEASE更新到v2.1.6.RELEASE,问题似乎就消失了。我查看了changelog of this release,在弹簧数据常见问题中与this bug相关的this bug是此问题的根本原因。升级spring-data-commons库后,我能够解决此问题。