From 442f5e60ddcbbefc16cd7eb076a895774dca4143 Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Wed, 20 Sep 2017 19:02:03 +0300 Subject: [PATCH] HHH-11863 - Implement REF_CURSOR support for StoredProcedureQuery.getOutputParameterValue(4); --- databases.gradle | 7 + .../org/hibernate/cfg/AvailableSettings.java | 2 +- .../AbstractParameterRegistrationImpl.java | 13 +- .../internal/ProcedureOutputsImpl.java | 4 +- .../result/internal/OutputsImpl.java | 5 + .../result/internal/ResultSetOutputImpl.java | 14 +- .../procedure/MySQLStoredProcedureTest.java | 134 +++++---- .../procedure/OracleStoredProcedureTest.java | 247 ++++++++++++----- .../PostgreSQLStoredProcedureTest.java | 256 ++++++++++-------- 9 files changed, 414 insertions(+), 268 deletions(-) diff --git a/databases.gradle b/databases.gradle index 0184ab7b57..f083801fae 100644 --- a/databases.gradle +++ b/databases.gradle @@ -37,6 +37,13 @@ ext { 'jdbc.pass' : 'hibernate_orm_test', 'jdbc.url' : 'jdbc:postgresql:hibernate_orm_test' ], + pgsql_docker : [ + 'db.dialect' : 'org.hibernate.dialect.PostgreSQL95Dialect', + 'jdbc.driver': 'org.postgresql.Driver', + 'jdbc.user' : 'hibernate_orm_test', + 'jdbc.pass' : 'hibernate_orm_test', + 'jdbc.url' : 'jdbc:postgresql://127.0.0.1/hibernate_orm_test' + ], mysql : [ 'db.dialect' : 'org.hibernate.dialect.MySQL57Dialect', 'jdbc.driver': 'com.mysql.jdbc.Driver', diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index a8a12c0e68..3ff3d7cd2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -1570,7 +1570,7 @@ public interface AvailableSettings { * Global setting for whether NULL parameter bindings should be passed to database * procedure/function calls as part of {@link org.hibernate.procedure.ProcedureCall} * handling. Implicitly Hibernate will not pass the NULL, the intention being to allow - * any default argumnet values to be applied. + * any default argument values to be applied. *

* This defines a global setting, which can them be controlled per parameter via * {@link org.hibernate.procedure.ParameterRegistration#enablePassingNulls(boolean)} diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/AbstractParameterRegistrationImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/AbstractParameterRegistrationImpl.java index 457185f843..31d3c5a257 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/AbstractParameterRegistrationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/AbstractParameterRegistrationImpl.java @@ -8,6 +8,7 @@ package org.hibernate.procedure.internal; import java.sql.CallableStatement; import java.sql.SQLException; +import java.sql.Types; import java.util.Calendar; import java.util.Date; import javax.persistence.ParameterMode; @@ -117,11 +118,12 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR this.type = type; if ( mode == ParameterMode.REF_CURSOR ) { - return; + this.sqlTypes = new int[]{ Types.REF_CURSOR }; + } + else { + this.passNulls = initialPassNullsSetting; + setHibernateType( hibernateType ); } - - this.passNulls = initialPassNullsSetting; - setHibernateType( hibernateType ); } private AbstractParameterRegistrationImpl( @@ -385,9 +387,6 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR if ( mode == ParameterMode.IN ) { throw new ParameterMisuseException( "IN parameter not valid for output extraction" ); } - else if ( mode == ParameterMode.REF_CURSOR ) { - throw new ParameterMisuseException( "REF_CURSOR parameters should be accessed via results" ); - } // TODO: sqlTypesToUse.length > 1 does not seem to have a working use case (HHH-10769). // For now, if sqlTypes.length > 1 with a named parameter, then extract diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java index 3849161f90..5552e1c66c 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java @@ -8,6 +8,8 @@ package org.hibernate.procedure.internal; import java.sql.CallableStatement; import java.sql.ResultSet; +import java.util.List; +import java.util.function.Supplier; import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport; import org.hibernate.procedure.ParameterRegistration; @@ -90,7 +92,7 @@ public class ProcedureOutputsImpl extends OutputsImpl implements ProcedureOutput .getService( RefCursorSupport.class ) .getResultSet( ProcedureOutputsImpl.this.callableStatement, refCursorParam.getPosition() ); } - return buildResultSetOutput( extractResults( resultSet ) ); + return buildResultSetOutput( () -> extractResults( resultSet ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java index 1ff43316a4..97a13b2ae4 100644 --- a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.hibernate.JDBCException; import org.hibernate.engine.spi.QueryParameters; @@ -201,6 +202,10 @@ public class OutputsImpl implements Outputs { return new ResultSetOutputImpl( list ); } + protected Output buildResultSetOutput(Supplier listSupplier) { + return new ResultSetOutputImpl( listSupplier ); + } + protected Output buildUpdateCountOutput(int updateCount) { return new UpdateCountOutputImpl( updateCount ); } diff --git a/hibernate-core/src/main/java/org/hibernate/result/internal/ResultSetOutputImpl.java b/hibernate-core/src/main/java/org/hibernate/result/internal/ResultSetOutputImpl.java index fa85ace480..0917833ef6 100644 --- a/hibernate-core/src/main/java/org/hibernate/result/internal/ResultSetOutputImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/result/internal/ResultSetOutputImpl.java @@ -6,7 +6,11 @@ */ package org.hibernate.result.internal; +import java.sql.ResultSet; import java.util.List; +import java.util.function.Supplier; + +import javax.enterprise.inject.spi.Producer; import org.hibernate.result.ResultSetOutput; @@ -16,10 +20,14 @@ import org.hibernate.result.ResultSetOutput; * @author Steve Ebersole */ class ResultSetOutputImpl implements ResultSetOutput { - private final List results; + private final Supplier resultSetSupplier; public ResultSetOutputImpl(List results) { - this.results = results; + this.resultSetSupplier = () -> results; + } + + public ResultSetOutputImpl(Supplier resultSetSupplier) { + this.resultSetSupplier = resultSetSupplier; } @Override @@ -30,7 +38,7 @@ class ResultSetOutputImpl implements ResultSetOutput { @Override @SuppressWarnings("unchecked") public List getResultList() { - return results; + return resultSetSupplier.get(); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/test/procedure/MySQLStoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/test/procedure/MySQLStoredProcedureTest.java index f6a07b8f3a..2500c4d020 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/procedure/MySQLStoredProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/procedure/MySQLStoredProcedureTest.java @@ -40,8 +40,8 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas @Override protected Class[] getAnnotatedClasses() { return new Class[] { - Person.class, - Phone.class, + Person.class, + Phone.class, }; } @@ -54,51 +54,50 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas try { Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { - Statement statement = null; - try { - statement = connection.createStatement(); - statement.executeUpdate( - "CREATE PROCEDURE sp_count_phones (" + - " IN personId INT, " + - " OUT phoneCount INT " + - ") " + - "BEGIN " + - " SELECT COUNT(*) INTO phoneCount " + - " FROM Phone p " + - " WHERE p.person_id = personId; " + - "END" - ); - statement.executeUpdate( - "CREATE PROCEDURE sp_phones(IN personId INT) " + - "BEGIN " + - " SELECT * " + - " FROM Phone " + - " WHERE person_id = personId; " + - "END" - ); - statement.executeUpdate( - "CREATE FUNCTION fn_count_phones(personId integer) " + - "RETURNS integer " + - "DETERMINISTIC " + - "READS SQL DATA " + - "BEGIN " + - " DECLARE phoneCount integer; " + - " SELECT COUNT(*) INTO phoneCount " + - " FROM Phone p " + - " WHERE p.person_id = personId; " + - " RETURN phoneCount; " + - "END" - ); - } finally { - if ( statement != null ) { - statement.close(); - } + session.doWork( connection -> { + Statement statement = null; + try { + statement = connection.createStatement(); + statement.executeUpdate( + "CREATE PROCEDURE sp_count_phones (" + + " IN personId INT, " + + " OUT phoneCount INT " + + ") " + + "BEGIN " + + " SELECT COUNT(*) INTO phoneCount " + + " FROM Phone p " + + " WHERE p.person_id = personId; " + + "END" + ); + + statement.executeUpdate( + "CREATE PROCEDURE sp_phones(IN personId INT) " + + "BEGIN " + + " SELECT * " + + " FROM Phone " + + " WHERE person_id = personId; " + + "END" + ); + + statement.executeUpdate( + "CREATE FUNCTION fn_count_phones(personId integer) " + + "RETURNS integer " + + "DETERMINISTIC " + + "READS SQL DATA " + + "BEGIN " + + " DECLARE phoneCount integer; " + + " SELECT COUNT(*) INTO phoneCount " + + " FROM Phone p " + + " WHERE p.person_id = personId; " + + " RETURN phoneCount; " + + "END" + ); + } finally { + if ( statement != null ) { + statement.close(); } } - } ); + } ); } finally { entityManager.getTransaction().rollback(); @@ -140,16 +139,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas try { Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_count_phones" ); - } - catch (SQLException ignore) { - } + session.doWork( connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_count_phones" ); } - } ); + catch (SQLException ignore) { + } + } ); } finally { entityManager.getTransaction().rollback(); @@ -161,16 +157,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas try { Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_phones" ); - } - catch (SQLException ignore) { - } + session.doWork( connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_phones" ); } - } ); + catch (SQLException ignore) { + } + } ); } finally { entityManager.getTransaction().rollback(); @@ -182,16 +175,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas try { Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( "DROP FUNCTION IF EXISTS fn_count_phones" ); - } - catch (SQLException ignore) { - } + session.doWork( connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( "DROP FUNCTION IF EXISTS fn_count_phones" ); } - } ); + catch (SQLException ignore) { + } + } ); } finally { entityManager.getTransaction().rollback(); diff --git a/hibernate-core/src/test/java/org/hibernate/test/procedure/OracleStoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/test/procedure/OracleStoredProcedureTest.java index d4de68669f..b9ce162dee 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/procedure/OracleStoredProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/procedure/OracleStoredProcedureTest.java @@ -11,8 +11,13 @@ import java.sql.Types; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; +import javax.persistence.Entity; import javax.persistence.EntityManager; +import javax.persistence.Id; +import javax.persistence.NamedStoredProcedureQueries; +import javax.persistence.NamedStoredProcedureQuery; import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; import javax.persistence.StoredProcedureQuery; import org.hibernate.Session; @@ -25,12 +30,17 @@ import org.hibernate.result.ResultSetOutput; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.transaction.TransactionUtil; import org.junit.After; import org.junit.Before; import org.junit.Test; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -44,9 +54,34 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa return new Class[] { Person.class, Phone.class, + IdHolder.class }; } + @NamedStoredProcedureQueries({ + @NamedStoredProcedureQuery( + name = "singleRefCursor", + procedureName = "singleRefCursor", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) + } + ), + @NamedStoredProcedureQuery( + name = "outAndRefCursor", + procedureName = "outAndRefCursor", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, type = Long.class), + } + ) + }) + @Entity(name = "IdHolder") + public static class IdHolder { + + @Id + Long id; + } + @Before public void init() { EntityManager entityManager = createEntityManager(); @@ -55,79 +90,95 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa try { Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { - Statement statement = null; - try { - statement = connection.createStatement(); - statement.executeUpdate( - "CREATE OR REPLACE PROCEDURE sp_count_phones ( " + - " personId IN NUMBER, " + - " phoneCount OUT NUMBER ) " + - "AS " + - "BEGIN " + - " SELECT COUNT(*) INTO phoneCount " + - " FROM phone " + - " WHERE person_id = personId; " + - "END;" - ); - statement.executeUpdate( - "CREATE OR REPLACE PROCEDURE sp_person_phones ( " + - " personId IN NUMBER, " + - " personPhones OUT SYS_REFCURSOR ) " + - "AS " + - "BEGIN " + - " OPEN personPhones FOR " + - " SELECT *" + - " FROM phone " + - " WHERE person_id = personId; " + - "END;" - ); - statement.executeUpdate( - "CREATE OR REPLACE FUNCTION fn_count_phones ( " + - " personId IN NUMBER ) " + - " RETURN NUMBER " + - "IS " + - " phoneCount NUMBER; " + - "BEGIN " + - " SELECT COUNT(*) INTO phoneCount " + - " FROM phone " + - " WHERE person_id = personId; " + - " RETURN( phoneCount ); " + - "END;" - ); - statement.executeUpdate( - "CREATE OR REPLACE FUNCTION fn_person_and_phones ( " + - " personId IN NUMBER ) " + - " RETURN SYS_REFCURSOR " + - "IS " + - " personAndPhones SYS_REFCURSOR; " + - "BEGIN " + - " OPEN personAndPhones FOR " + - " SELECT " + - " pr.id AS \"pr.id\", " + - " pr.name AS \"pr.name\", " + - " pr.nickName AS \"pr.nickName\", " + - " pr.address AS \"pr.address\", " + - " pr.createdOn AS \"pr.createdOn\", " + - " pr.version AS \"pr.version\", " + - " ph.id AS \"ph.id\", " + - " ph.person_id AS \"ph.person_id\", " + - " ph.phone_number AS \"ph.phone_number\" " + - " FROM person pr " + - " JOIN phone ph ON pr.id = ph.person_id " + - " WHERE pr.id = personId; " + - " RETURN personAndPhones; " + - "END;" - ); - } finally { - if ( statement != null ) { - statement.close(); - } - } - } - } ); + session.doWork( connection -> { + Statement statement = null; + try { + statement = connection.createStatement(); + statement.executeUpdate( + "CREATE OR REPLACE PROCEDURE sp_count_phones ( " + + " personId IN NUMBER, " + + " phoneCount OUT NUMBER ) " + + "AS " + + "BEGIN " + + " SELECT COUNT(*) INTO phoneCount " + + " FROM phone " + + " WHERE person_id = personId; " + + "END;" + ); + statement.executeUpdate( + "CREATE OR REPLACE PROCEDURE sp_person_phones ( " + + " personId IN NUMBER, " + + " personPhones OUT SYS_REFCURSOR ) " + + "AS " + + "BEGIN " + + " OPEN personPhones FOR " + + " SELECT *" + + " FROM phone " + + " WHERE person_id = personId; " + + "END;" + ); + statement.executeUpdate( + "CREATE OR REPLACE FUNCTION fn_count_phones ( " + + " personId IN NUMBER ) " + + " RETURN NUMBER " + + "IS " + + " phoneCount NUMBER; " + + "BEGIN " + + " SELECT COUNT(*) INTO phoneCount " + + " FROM phone " + + " WHERE person_id = personId; " + + " RETURN( phoneCount ); " + + "END;" + ); + statement.executeUpdate( + "CREATE OR REPLACE FUNCTION fn_person_and_phones ( " + + " personId IN NUMBER ) " + + " RETURN SYS_REFCURSOR " + + "IS " + + " personAndPhones SYS_REFCURSOR; " + + "BEGIN " + + " OPEN personAndPhones FOR " + + " SELECT " + + " pr.id AS \"pr.id\", " + + " pr.name AS \"pr.name\", " + + " pr.nickName AS \"pr.nickName\", " + + " pr.address AS \"pr.address\", " + + " pr.createdOn AS \"pr.createdOn\", " + + " pr.version AS \"pr.version\", " + + " ph.id AS \"ph.id\", " + + " ph.person_id AS \"ph.person_id\", " + + " ph.phone_number AS \"ph.phone_number\" " + + " FROM person pr " + + " JOIN phone ph ON pr.id = ph.person_id " + + " WHERE pr.id = personId; " + + " RETURN personAndPhones; " + + "END;" + ); + statement.executeUpdate( + "CREATE OR REPLACE " + + "PROCEDURE singleRefCursor(p_recordset OUT SYS_REFCURSOR) AS " + + " BEGIN " + + " OPEN p_recordset FOR " + + " SELECT 1 as id " + + " FROM dual; " + + " END; " + ); + statement.executeUpdate( + "CREATE OR REPLACE " + + "PROCEDURE outAndRefCursor(p_recordset OUT SYS_REFCURSOR, p_value OUT NUMBER) AS " + + " BEGIN " + + " OPEN p_recordset FOR " + + " SELECT 1 as id " + + " FROM dual; " + + " SELECT 1 INTO p_value FROM dual; " + + " END; " + ); + } finally { + if ( statement != null ) { + statement.close(); + } + } + } ); } finally { entityManager.getTransaction().rollback(); @@ -357,4 +408,54 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa entityManager.close(); } } + + @Test + @TestForIssue( jiraKey = "HHH-11863") + public void testSysRefCursorAsOutParameter() { + + doInJPA( this::entityManagerFactory, entityManager -> { + StoredProcedureQuery function = entityManager.createNamedStoredProcedureQuery("singleRefCursor"); + + function.execute(); + + assertFalse( function.hasMoreResults() ); + Long value = null; + + try ( ResultSet resultSet = (ResultSet) function.getOutputParameterValue( 1 ) ) { + while ( resultSet.next() ) { + value = resultSet.getLong( 1 ); + } + } + catch (SQLException e) { + fail(e.getMessage()); + } + + assertEquals( Long.valueOf( 1 ), value ); + } ); + } + + @Test + @TestForIssue( jiraKey = "HHH-11863") + public void testOutAndSysRefCursorAsOutParameter() { + + doInJPA( this::entityManagerFactory, entityManager -> { + StoredProcedureQuery function = entityManager.createNamedStoredProcedureQuery("outAndRefCursor"); + + function.execute(); + + assertFalse( function.hasMoreResults() ); + Long value = null; + + try ( ResultSet resultSet = (ResultSet) function.getOutputParameterValue( 1 ) ) { + while ( resultSet.next() ) { + value = resultSet.getLong( 1 ); + } + } + catch (SQLException e) { + fail(e.getMessage()); + } + + assertEquals( value, function.getOutputParameterValue( 2 ) ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/procedure/PostgreSQLStoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/test/procedure/PostgreSQLStoredProcedureTest.java index 662cad9623..e256c1afa4 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/procedure/PostgreSQLStoredProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/procedure/PostgreSQLStoredProcedureTest.java @@ -1,7 +1,7 @@ package org.hibernate.test.procedure; import java.sql.CallableStatement; -import java.sql.Connection; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; @@ -10,21 +10,22 @@ import java.sql.Types; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; -import javax.persistence.EntityManager; import javax.persistence.ParameterMode; import javax.persistence.StoredProcedureQuery; import org.hibernate.Session; import org.hibernate.dialect.PostgreSQL81Dialect; -import org.hibernate.jdbc.ReturningWork; -import org.hibernate.jdbc.Work; import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; import org.junit.Before; import org.junit.Test; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; /** * @author Vlad Mihalcea @@ -36,19 +37,16 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe protected Class[] getAnnotatedClasses() { return new Class[] { Person.class, - Phone.class, + Phone.class }; } @Before public void init() { - EntityManager entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - Session session = entityManager.unwrap( Session.class ); + doInJPA( this::entityManagerFactory, entityManager -> { + Session session = entityManager.unwrap( Session.class ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { + session.doWork( connection -> { Statement statement = null; try { statement = connection.createStatement(); @@ -61,19 +59,13 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe statement.close(); } } - } + } ); } ); - entityManager.getTransaction().commit(); - entityManager.close(); + doInJPA( this::entityManagerFactory, entityManager -> { + Session session = entityManager.unwrap( Session.class ); - entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - session = entityManager.unwrap( Session.class ); - - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { + session.doWork( connection -> { Statement statement = null; try { statement = connection.createStatement(); @@ -86,19 +78,32 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe statement.close(); } } - } + } ); } ); - entityManager.getTransaction().commit(); - entityManager.close(); + doInJPA( this::entityManagerFactory, entityManager -> { + Session session = entityManager.unwrap( Session.class ); - entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - session = entityManager.unwrap( Session.class ); + session.doWork( connection -> { + Statement statement = null; + try { + statement = connection.createStatement(); + statement.executeUpdate( "DROP FUNCTION singleRefCursor(bigint)" ); + } + catch (SQLException ignore) { + } + finally { + if ( statement != null ) { + statement.close(); + } + } + } ); + } ); - session.doWork( new Work() { - @Override - public void execute(Connection connection) throws SQLException { + doInJPA( this::entityManagerFactory, entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + session.doWork( connection -> { Statement statement = null; try { statement = connection.createStatement(); @@ -133,48 +138,52 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe "$BODY$ " + "LANGUAGE plpgsql" ); + + statement.executeUpdate( + "CREATE OR REPLACE FUNCTION singleRefCursor() " + + " RETURNS REFCURSOR AS " + + "$BODY$ " + + " DECLARE " + + " p_recordset REFCURSOR; " + + " BEGIN " + + " OPEN p_recordset FOR SELECT 1; " + + " RETURN p_recordset; " + + " END; " + + "$BODY$ " + + "LANGUAGE plpgsql;" + ); } finally { if ( statement != null ) { statement.close(); } } - } + } ); } ); - entityManager.getTransaction().commit(); - entityManager.close(); + doInJPA( this::entityManagerFactory, entityManager -> { + Person person1 = new Person( "John Doe" ); + person1.setNickName( "JD" ); + person1.setAddress( "Earth" ); + person1.setCreatedOn( Timestamp.from( LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ) ) ); - entityManager = createEntityManager(); - entityManager.getTransaction().begin(); + entityManager.persist( person1 ); - Person person1 = new Person( "John Doe" ); - person1.setNickName( "JD" ); - person1.setAddress( "Earth" ); - person1.setCreatedOn( Timestamp.from( LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ) ) ); + Phone phone1 = new Phone( "123-456-7890" ); + phone1.setId( 1L ); - entityManager.persist( person1 ); + person1.addPhone( phone1 ); - Phone phone1 = new Phone( "123-456-7890" ); - phone1.setId( 1L ); + Phone phone2 = new Phone( "098_765-4321" ); + phone2.setId( 2L ); - person1.addPhone( phone1 ); - - Phone phone2 = new Phone( "098_765-4321" ); - phone2.setId( 2L ); - - person1.addPhone( phone2 ); - - entityManager.getTransaction().commit(); - entityManager.close(); - } + person1.addPhone( phone2 ); + } ); + } @Test public void testStoredProcedureOutParameter() { - EntityManager entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - - try { + doInJPA( this::entityManagerFactory, entityManager -> { StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "sp_count_phones" ); query.registerStoredProcedureParameter( "personId", Long.class, ParameterMode.IN ); query.registerStoredProcedureParameter( "phoneCount", Long.class, ParameterMode.OUT ); @@ -184,19 +193,12 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe query.execute(); Long phoneCount = (Long) query.getOutputParameterValue( "phoneCount" ); assertEquals( Long.valueOf( 2 ), phoneCount ); - } - finally { - entityManager.getTransaction().rollback(); - entityManager.close(); - } + } ); } @Test public void testStoredProcedureRefCursor() { - EntityManager entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - - try { + doInJPA( this::entityManagerFactory, entityManager -> { StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "fn_phones" ); query.registerStoredProcedureParameter( 1, void.class, ParameterMode.REF_CURSOR ); query.registerStoredProcedureParameter( 2, Long.class, ParameterMode.IN ); @@ -205,56 +207,38 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe List phones = query.getResultList(); assertEquals( 2, phones.size() ); - } - finally { - entityManager.getTransaction().rollback(); - entityManager.close(); - } + } ); } @Test public void testFunctionWithJDBC() { - EntityManager entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - - try { + doInJPA( this::entityManagerFactory, entityManager -> { Session session = entityManager.unwrap( Session.class ); - Long phoneCount = session.doReturningWork( new ReturningWork() { - @Override - public Long execute(Connection connection) throws SQLException { - CallableStatement function = null; - try { - function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" ); - function.registerOutParameter( 1, Types.BIGINT ); - function.setLong( 2, 1L ); - function.execute(); - return function.getLong( 1 ); - } - finally { - if ( function != null ) { - function.close(); - } + Long phoneCount = session.doReturningWork( connection -> { + CallableStatement function = null; + try { + function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" ); + function.registerOutParameter( 1, Types.BIGINT ); + function.setLong( 2, 1L ); + function.execute(); + return function.getLong( 1 ); + } + finally { + if ( function != null ) { + function.close(); } } } ); assertEquals( Long.valueOf( 2 ), phoneCount ); - } - finally { - entityManager.getTransaction().rollback(); - entityManager.close(); - } + } ); } @Test public void testFunctionWithJDBCByName() { - EntityManager entityManager = createEntityManager(); - entityManager.getTransaction().begin(); - - try { - Session session = entityManager.unwrap( Session.class ); - Long phoneCount = session.doReturningWork( new ReturningWork() { - @Override - public Long execute(Connection connection) throws SQLException { + doInJPA( this::entityManagerFactory, entityManager -> { + try { + Session session = entityManager.unwrap( Session.class ); + Long phoneCount = session.doReturningWork( connection -> { CallableStatement function = null; try { function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" ); @@ -268,15 +252,65 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe function.close(); } } + } ); + assertEquals( Long.valueOf( 2 ), phoneCount ); + } catch (Exception e) { + assertEquals( SQLFeatureNotSupportedException.class, e.getCause().getClass() ); + } + } ); + } + + @Test + @TestForIssue( jiraKey = "HHH-11863") + public void testSysRefCursorAsOutParameter() { + + doInJPA( this::entityManagerFactory, entityManager -> { + Long value = null; + + Session session = entityManager.unwrap( Session.class ); + + try(ResultSet resultSet = session.doReturningWork( connection -> { + CallableStatement function = null; + try { + function = connection.prepareCall( "{ ? = call singleRefCursor() }" ); + function.registerOutParameter( 1, Types.REF_CURSOR ); + function.execute(); + return (ResultSet) function.getObject( 1 ); } - } ); - assertEquals( Long.valueOf( 2 ), phoneCount ); - } catch (Exception e) { - assertEquals( SQLFeatureNotSupportedException.class, e.getCause().getClass() ); - } - finally { - entityManager.getTransaction().rollback(); - entityManager.close(); - } + finally { + if ( function != null ) { + function.close(); + } + } + } )) { + while ( resultSet.next() ) { + value = resultSet.getLong( 1 ); + } + } + catch (Exception e) { + fail(e.getMessage()); + } + assertEquals( Long.valueOf( 1 ), value ); + + + StoredProcedureQuery function = entityManager.createStoredProcedureQuery( "singleRefCursor" ); + function.registerStoredProcedureParameter( 1, void.class, ParameterMode.REF_CURSOR ); + + function.execute(); + + assertFalse( function.hasMoreResults() ); + + value = null; + try ( ResultSet resultSet = (ResultSet) function.getOutputParameterValue( 1 ) ) { + while ( resultSet.next() ) { + value = resultSet.getLong( 1 ); + } + } + catch (SQLException e) { + fail(e.getMessage()); + } + + assertEquals( Long.valueOf( 1 ), value ); + } ); } }