簡體   English   中英

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(例如,第二頁),我就會得到一個對象列表,這個列表中的每個對象似乎都包含兩個 BigDecimal,其中第一個包含來自數據庫的值,第二個 BigDecimal 似乎包含成為這一行的位置。

顯然我得到了這個例外

java.lang.ClassCastException: 類 [Ljava.lang.Object; 不能強制轉換為 java.math.BigDecimal 類

有人可以解釋這種差異,以及如何修復它以始終返回 BigDecimals 列表? 謝謝你。

更新 1 :我創建了一個示例項目來重現此問題。 我只能使用 Oracle 數據庫重現此問題。 使用 H2 數據庫,它運行良好,並且我始終得到一個與頁碼無關的 BigDecimals 列表。

更新 2 :我還使用 H2創建了一個示例項目,它可以在沒有此問題的情況下工作。

您遇到的問題是您的 OracleDialect 將一列添加到其選定的 ResultSet。 它包裝了您在 SternK 的回答中討論的正在運行的查詢。

如果您正在使用 Hibernate SessionFactory 和 Session 接口,那么您要尋找的函數將是“addScalar”方法。 不幸的是,在純 JPA 中似乎沒有實現(請參閱此處提出的問題: Does JPA has 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. 您可以修改您的 oracle 方言(如 SternK 建議的那樣)。 這將利用替代的 oracle-paging 實現。

  2. 如果您不反對在 JPA 實現中使用特定於休眠的方面,那么您可以利用 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);

這確實具有顯式告訴 hibernate 的優點,您只對 ResultSet 的“id”列感興趣,並且 hibernate 需要顯式地將返回的對象轉換為 BigDecimal,如果 JDBC 驅動程序決定不同的類型作為默認值會更合適。

您的問題的根本原因在於分頁如何在您的 hibernate oracle 方言中實現

有兩種情況:

  1. 當我們設置了setFirstResult(0) ,將生成以下 sql:
-- setMaxResults(5).setFirstResult(0)
select * from (
  select test_id from TST_MY_TEST -- this is your initial query
) 
where rownum <= 5;

如您所見,此查詢返回與初始查詢完全相同的列列表,因此您在這種情況下沒有問題。

  1. 當我們將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) 或更高版本,您可以通過這種方式使用新的行限制子句在您的方言中覆蓋此行為:

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

PS 另見HHH-12087

PPS 我通過刪除檢查顯示FOR UPDATE子句簡化了AbstractLimitHandler的實現。 我認為在這種情況下,通過這次檢查,我們不會有任何好處。

例如對於以下情況:

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 列而不是僅一列。 不受歡迎的是這個rownum 對於其他方言,沒有這樣的問題。

我建議您嘗試其他 oracle 方言版本,如果它們都不起作用,我的最后提示是嘗試自己進行分頁。

經過大量不同版本的不同 spring 庫的跟蹤后,我終於能夠找出問題所在。 在我的一次嘗試中,一旦我將 spring-data-commons 庫從 v2.1.5.RELEASE 更新到 v2.1.6.RELEASE,問題似乎就消失了。 我查看了這個版本的更新日志這個 bug與 spring-data-commons 中的這個 bug相關,是這個問題的根本原因。 升級 spring-data-commons 庫后,我能夠解決這個問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM