Nejlepší způsob volání uložené procedury pomocí JPA a Hibernate
Úvod
V tomto článku se naučíte, jak nejlépe volat uloženou proceduru při použití JPA a Hibernate, aby byly základní zdroje JDBC uvolněny co nejdříve.
Rozhodl jsem se napsat tento článek, protože způsob, jakým Hibernate zpracovává uložené procedury, může vést k ORA-01000: maximum open cursors exceeded
problémy na Oracle, jak je vysvětleno v tomto vláknu fóra Hibernate nebo v otázce StackOverflow.
Jak funguje volání uložené procedury s JPA a Hibernate
Pro volání uložené procedury nebo databázové funkce pomocí JPA můžete použít StoredProcedureQuery
jak ukazuje následující příklad:
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");
V zákulisí, StoredProcedureQuery
rozhraní je rozšířeno o Hibernate specifické ProcedureCall
rozhraní, takže předchozí příklad můžeme přepsat takto:
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");
Při volání execute
metoda na JPA StoredProcedureQuery
nebo outputs().getCurrent()
na Hibernate ProcedureCall
, Hibernate provede následující akce:
Všimněte si, že JDBC CallableStatement
je připraven a uložen v příslušném ProcedureOutputsImpl
objekt. Při volání getOutputParameterValue
metoda Hibernate použije základní CallableStatement
k načtení OUT
parametr.
Z tohoto důvodu je základní JDBC CallableStatement
zůstane otevřená i po provedení uložené procedury a načtení OUT
nebo REF_CURSOR
parametry.
Nyní je ve výchozím nastavení CallableStatement
je uzavřen po ukončení aktuálně probíhající databázové transakce, a to buď voláním commit
nebo rollback
.
Doba testování
Chcete-li toto chování ověřit, zvažte následující testovací případ:
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());
Všimněte si, že CallableStatement
je stále otevřená i po zavolání execute
nebo načtením commentCount
OUT
parametr. Pouze po zavolání release
na ProcedureOutputs
objekt, CallableStatement
bude zavřeno.
Uzavření příkazu JDBC co nejdříve
Proto zavřít JDBC CallableStatement
co nejdříve zavolejte release
po načtení všech požadovaných dat z uložené procedury:
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());
Volání release
metoda na přidruženém ProcedureOutputs
objekt v posledním bloku zajišťuje, že JDBC CallableStatement
je uzavřen bez ohledu na výsledek volání uložené procedury.
Nyní volání release
ručně je to trochu zdlouhavé, tak jsem se rozhodl vytvořit problém HHH-13215 Jira, který jsem integroval do větve Hibernate ORM 6.
Proto od Hibernate 6 výše můžete předchozí příklad přepsat takto:
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"); } });
Mnohem lepší, že?
Vytvořením ProcedureCall
rozhraní rozšířit AutoClosable
, mohli bychom použít příkaz try-with-resource Java, takže volání uložené procedury databáze by bylo méně podrobné a intuitivnější, pokud jde o dealokaci zdrojů JDBC.
Závěr
Uvolnění základního JDBC CallableStatement
co nejdříve je velmi důležité při volání uložené procedury s JPA a Hibernate, protože jinak budou kurzory databáze otevřené, dokud není aktuální transakce potvrzena nebo vrácena zpět.
Proto počínaje Hibernate ORM 6 byste měli použít blok try-finally. Mezitím pro Hibernate 5 a 4 byste měli použít blok try-finally k uzavření CallableStatement
ihned poté, co dokončíte načítání všech potřebných dat.