使用JPA和Hibernate呼叫儲存過程的最佳方法 - Vlad Mihalcea

banq發表於2019-02-17

在本文中,您將學習使用JPA和Hibernate時呼叫儲存過程的最佳方法,以便儘快釋放底層JDBC資源。
我決定寫這篇文章,因為Hibernate處理儲存過程的方式會導致ORA-01000: maximum open cursors exceededOracle 上出現問題,如本Hibernate論壇帖子StackOverflow問題所述

儲存過程呼叫如何與JPA和Hibernate一起使用
要使用JPA呼叫儲存過程或資料庫函式,可以使用StoredProcedureQuery如以下示例所示:

StoredProcedureQuery query = entityManager
.createStoredProcedureQuery("count_comments")
.registerStoredProcedureParameter(
    "postId", 
    Long.class, 
    ParameterMode.IN
)
.registerStoredProcedureParameter(
    "commentCount", 
    Long.class, 
    ParameterMode.OUT
)
.setParameter("postId", 1L);

query.execute();
Long commentCount = (Long) query
.getOutputParameterValue("commentCount");


在幕後,StoredProcedureQuery介面透過特定於Hibernate 的介面進行擴充套件ProcedureCall,因此我們可以像這樣重寫前面的示例:

ProcedureCall query = session
.createStoredProcedureCall("count_comments");

query.registerParameter(
    "postId", 
    Long.class, 
    ParameterMode.IN
)
.bindValue(1L);

query.registerParameter(
    "commentCount", 
    Long.class, 
    ParameterMode.OUT
);

Long commentCount = (Long) call
.getOutputs()
.getOutputParameterValue("commentCount");


在執行Hibernate的ProcedureCall的JPA的StoredProcedureQuery或outputs().getCurrent()時,Hibernate執行以下操作:
請注意,JDBC CallableStatement已準備好並儲存在關聯的ProcedureOutputsImpl物件中。在呼叫getOutputParameterValue方法時,Hibernate將使用底層CallableStatement來獲取OUT引數。

因此,CallableStatement即使在執行儲存過程並獲取OUT或REF_CURSOR引數之後,底層JDBC 仍保持開啟狀態。

現在,預設情況下,CallableStatement在當前正在執行的資料庫事務結束時將關閉,這是透過呼叫commit或rollback實現。

測試時間
要驗證此行為,請考慮以下測試用例:

StoredProcedureQuery query = entityManager
.createStoredProcedureQuery("count_comments")
.registerStoredProcedureParameter(
    "postId", 
    Long.class, 
    ParameterMode.IN
)
.registerStoredProcedureParameter(
    "commentCount", 
    Long.class, 
    ParameterMode.OUT
)
.setParameter("postId", 1L);

query.execute();

Long commentCount = (Long) query
.getOutputParameterValue("commentCount");

assertEquals(Long.valueOf(2), commentCount);

ProcedureOutputs procedureOutputs = query
.unwrap(ProcedureOutputs.class);

CallableStatement callableStatement = ReflectionUtils
.getFieldValue(
    procedureOutputs, 
    "callableStatement"
);

assertFalse(callableStatement.isClosed());

procedureOutputs.release();

assertTrue(callableStatement.isClosed());


請注意,CallableStatement即使在呼叫execute或獲取commentCount OUT引數後仍處於開啟狀態。只有呼叫完成後才釋放ProcedureOutputs的物件時,CallableStatement也會封閉。


儘快關閉JDBC語句
因此,要CallableStatement儘快關閉JDBC ,在儲存過程中獲取所需的所有資料後呼叫release:

StoredProcedureQuery query = entityManager
.createStoredProcedureQuery("count_comments")
.registerStoredProcedureParameter(
    "postId", 
    Long.class, 
    ParameterMode.IN
)
.registerStoredProcedureParameter(
    "commentCount", 
    Long.class, 
    ParameterMode.OUT
)
.setParameter("postId", 1L);

try {
    query.execute();
    
    Long commentCount = (Long) query
    .getOutputParameterValue("commentCount");

    assertEquals(Long.valueOf(2), commentCount);
} finally {
    query.unwrap(ProcedureOutputs.class).release();
}

CallableStatement callableStatement = ReflectionUtils
.getFieldValue(
    query.unwrap(ProcedureOutputs.class), 
    "callableStatement"
);
assertTrue(callableStatement.isClosed());


在finally塊中release的關聯ProcedureOutputs物件上呼叫方法可確保CallableStatement無論儲存過程呼叫的結果如何都關閉JDBC 。
現在,release手動呼叫有點乏味,所以我決定建立HHH-13215Jira問題,我將其整合到Hibernate ORM 6分支中。
因此,從Hibernate 6開始,您可以像這樣重寫前面的示例:

Long commentCount = doInJPA(entityManager -> {
    try(ProcedureCall query = entityManager
            .createStoredProcedureQuery("count_comments")
            .unwrap(ProcedureCall.class)) {
             
        return (Long) query
        .registerStoredProcedureParameter(
            "postId",
            Long.class,
            ParameterMode.IN
        )
        .registerStoredProcedureParameter(
            "commentCount",
            Long.class,
            ParameterMode.OUT
        )
        .setParameter("postId", 1L)
        .getOutputParameterValue("commentCount");
    }
});



好多了,對吧?
透過ProcedureCall擴充套件介面AutoClosable,我們可以使用try-with-resource Java語句,因此在解除分配JDBC資源時,呼叫資料庫儲存過程會更簡潔,更直觀。

結論
在CallableStatement使用JPA和Hibernate呼叫儲存過程時,儘快釋放底層JDBC 非常重要,否則,資料庫遊標將一直開啟,直到提交或回滾當前事務為止。
因此,從Hibernate ORM 6開始,您應該使用try-finally塊。同時,對於Hibernate 5和4,CallableStatement在完成獲取所需的所有資料後,應該使用try-finally塊關閉右側。

相關文章