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(示例,第二页),我就会得到一个对象列表,并且此列表中的每个对象似乎都包含两个 BigDecimals,其中第一个包含来自的值db,第二个BigDecimal好像是这一行的位置。
显然我遇到了这个异常
java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class java.math.BigDecimal
有人可以解释这个差异吗,以及如何将其固定为始终 return BigDecimals 列表?谢谢。
Update-1 :我创建了 a sample project 来重现这个问题。我只能使用 Oracle 数据库重现此问题。使用 H2 数据库,它运行良好,我始终得到一个与页码无关的 BigDecimals 列表。
Update-2 :我还创建了 a sample project with H2 并且没有这个问题。
我模拟了您的咨询,一切正常。我使用 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方言有关。出于未知原因,第二次调用 运行ning 的查询是:select * from ( select row_.*, rownum rownum_ from ( SELECT c.SHOP_ID FROM CUSTOMER c ) row_ where rownum <= ?) where rownum_ > ?
,这就是产生错误的原因,因为它查询 2 列而不是仅查询 1 列。不受欢迎的是这个rownum
。其他方言就没有这个问题。
我建议您尝试其他 oracle 方言版本,看看它们 none 是否有效,我最后的建议是尝试自己进行分页。
问题的根本原因在于休眠中分页的实现方式 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;
如您所见,此查询 returns 列列表与您的初始查询完全相同,因此您对这种情况没有问题。
- 当我们在非
0
值中设置setFirstResult
时,将生成以下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
如您所见,此查询 returns 列列表包含额外的 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 我通过删除 checking presents FOR UPDATE
子句简化了 AbstractLimitHandler
的实现。我认为在这种情况下,通过这种检查,我们不会有任何好处。
例如以下情况:
NativeQuery query = session.createNativeQuery(
"select test_id from TST_MY_TEST FOR UPDATE OF test_id"
).setMaxResults(5).setFirstResult(2);
hibernate (with 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.
在对不同 spring 库的不同版本进行了大量跟踪之后,我终于能够找出问题所在。在我的一次尝试中,当我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE 后,问题似乎就消失了。我查了一下spring-data-commons中的changelog of this release, and this bug, which is related to this bug,是这个问题的根本原因。我在升级 spring-data-commons 库后解决了这个问题。
您 运行 遇到的问题是您的 OracleDialect 向其选定的结果集中添加了一列。正如 SternK 的回答中所讨论的那样,它包装了您 运行 的查询。
如果您使用的是 Hibernate SessionFactory 和 Session 接口,那么您要寻找的函数就是“addScalar”方法。不幸的是,似乎没有纯 JPA 的实现(请参阅此处提出的问题:Does JPA have an equivalent to Hibernate SQLQuery.addScalar()?)。
我希望您当前的实现在 DB2、H2、HSQL、Postgres、MySQL(以及其他一些数据库引擎)中工作得很好。但是,在 Oracle 中,它向 ResultSet 添加了一个 row-number 列,这意味着 Hibernate 从 ResultSet 中获取了 2 列。在这种情况下,Hibernate 不执行任何查询解析,这意味着它只是将 ResultSet 解析到您的 List 中。由于它获得 2 个值,因此它将它们转换为 Object[] 而不是 BigDecimal.
作为一个警告,依靠 JDBC 驱动程序提供 expected-data-type 有点危险,因为 Hibernate 会询问 JDBC 驱动程序它建议哪个 data-type .在这种情况下,它建议使用 BigDecimal,但在某些条件下和某些实现将被允许 return Double 或其他一些类型。
那么你有几个选择。
您可以修改 oracle-dialect(如 SternK 所建议的)。这将利用备用 oracle-paging 实现。
如果您不反对在 JPA 实现中包含 hibnerate-specific 方面,那么您可以利用 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”列感兴趣,而 hibernate 需要显式地将 returned 对象转换为 BigDecimal, JDBC-driver 是否应该决定另一种类型作为默认值更合适。
我正在尝试将分页与 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: class [Ljava.lang.Object; cannot be cast to class java.math.BigDecimal
有人可以解释这个差异吗,以及如何将其固定为始终 return BigDecimals 列表?谢谢。
Update-1 :我创建了 a sample project 来重现这个问题。我只能使用 Oracle 数据库重现此问题。使用 H2 数据库,它运行良好,我始终得到一个与页码无关的 BigDecimals 列表。
Update-2 :我还创建了 a sample project with H2 并且没有这个问题。
我模拟了您的咨询,一切正常。我使用 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方言有关。出于未知原因,第二次调用 运行ning 的查询是:select * from ( select row_.*, rownum rownum_ from ( SELECT c.SHOP_ID FROM CUSTOMER c ) row_ where rownum <= ?) where rownum_ > ?
,这就是产生错误的原因,因为它查询 2 列而不是仅查询 1 列。不受欢迎的是这个rownum
。其他方言就没有这个问题。
我建议您尝试其他 oracle 方言版本,看看它们 none 是否有效,我最后的建议是尝试自己进行分页。
问题的根本原因在于休眠中分页的实现方式 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;
如您所见,此查询 returns 列列表与您的初始查询完全相同,因此您对这种情况没有问题。
- 当我们在非
0
值中设置setFirstResult
时,将生成以下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
如您所见,此查询 returns 列列表包含额外的 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 我通过删除 checking presents FOR UPDATE
子句简化了 AbstractLimitHandler
的实现。我认为在这种情况下,通过这种检查,我们不会有任何好处。
例如以下情况:
NativeQuery query = session.createNativeQuery(
"select test_id from TST_MY_TEST FOR UPDATE OF test_id"
).setMaxResults(5).setFirstResult(2);
hibernate (with 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.
在对不同 spring 库的不同版本进行了大量跟踪之后,我终于能够找出问题所在。在我的一次尝试中,当我将 spring-data-commons 库从 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE 后,问题似乎就消失了。我查了一下spring-data-commons中的changelog of this release, and this bug, which is related to this bug,是这个问题的根本原因。我在升级 spring-data-commons 库后解决了这个问题。
您 运行 遇到的问题是您的 OracleDialect 向其选定的结果集中添加了一列。正如 SternK 的回答中所讨论的那样,它包装了您 运行 的查询。
如果您使用的是 Hibernate SessionFactory 和 Session 接口,那么您要寻找的函数就是“addScalar”方法。不幸的是,似乎没有纯 JPA 的实现(请参阅此处提出的问题:Does JPA have an equivalent to Hibernate SQLQuery.addScalar()?)。
我希望您当前的实现在 DB2、H2、HSQL、Postgres、MySQL(以及其他一些数据库引擎)中工作得很好。但是,在 Oracle 中,它向 ResultSet 添加了一个 row-number 列,这意味着 Hibernate 从 ResultSet 中获取了 2 列。在这种情况下,Hibernate 不执行任何查询解析,这意味着它只是将 ResultSet 解析到您的 List 中。由于它获得 2 个值,因此它将它们转换为 Object[] 而不是 BigDecimal.
作为一个警告,依靠 JDBC 驱动程序提供 expected-data-type 有点危险,因为 Hibernate 会询问 JDBC 驱动程序它建议哪个 data-type .在这种情况下,它建议使用 BigDecimal,但在某些条件下和某些实现将被允许 return Double 或其他一些类型。
那么你有几个选择。
您可以修改 oracle-dialect(如 SternK 所建议的)。这将利用备用 oracle-paging 实现。
如果您不反对在 JPA 实现中包含 hibnerate-specific 方面,那么您可以利用 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”列感兴趣,而 hibernate 需要显式地将 returned 对象转换为 BigDecimal, JDBC-driver 是否应该决定另一种类型作为默认值更合适。