HHH-11863 - Implement REF_CURSOR support for StoredProcedureQuery.getOutputParameterValue(4);

This commit is contained in:
Vlad Mihalcea 2017-09-20 19:02:03 +03:00
parent 287221e26e
commit 442f5e60dd
9 changed files with 414 additions and 268 deletions

View File

@ -37,6 +37,13 @@ ext {
'jdbc.pass' : 'hibernate_orm_test', 'jdbc.pass' : 'hibernate_orm_test',
'jdbc.url' : 'jdbc:postgresql: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 : [ mysql : [
'db.dialect' : 'org.hibernate.dialect.MySQL57Dialect', 'db.dialect' : 'org.hibernate.dialect.MySQL57Dialect',
'jdbc.driver': 'com.mysql.jdbc.Driver', 'jdbc.driver': 'com.mysql.jdbc.Driver',

View File

@ -1570,7 +1570,7 @@ public interface AvailableSettings {
* Global setting for whether NULL parameter bindings should be passed to database * Global setting for whether NULL parameter bindings should be passed to database
* procedure/function calls as part of {@link org.hibernate.procedure.ProcedureCall} * procedure/function calls as part of {@link org.hibernate.procedure.ProcedureCall}
* handling. Implicitly Hibernate will not pass the NULL, the intention being to allow * 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.
* <p/> * <p/>
* This defines a global setting, which can them be controlled per parameter via * This defines a global setting, which can them be controlled per parameter via
* {@link org.hibernate.procedure.ParameterRegistration#enablePassingNulls(boolean)} * {@link org.hibernate.procedure.ParameterRegistration#enablePassingNulls(boolean)}

View File

@ -8,6 +8,7 @@ package org.hibernate.procedure.internal;
import java.sql.CallableStatement; import java.sql.CallableStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import javax.persistence.ParameterMode; import javax.persistence.ParameterMode;
@ -117,11 +118,12 @@ public abstract class AbstractParameterRegistrationImpl<T> implements ParameterR
this.type = type; this.type = type;
if ( mode == ParameterMode.REF_CURSOR ) { 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( private AbstractParameterRegistrationImpl(
@ -385,9 +387,6 @@ public abstract class AbstractParameterRegistrationImpl<T> implements ParameterR
if ( mode == ParameterMode.IN ) { if ( mode == ParameterMode.IN ) {
throw new ParameterMisuseException( "IN parameter not valid for output extraction" ); 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). // 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 // For now, if sqlTypes.length > 1 with a named parameter, then extract

View File

@ -8,6 +8,8 @@ package org.hibernate.procedure.internal;
import java.sql.CallableStatement; import java.sql.CallableStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.List;
import java.util.function.Supplier;
import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport; import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport;
import org.hibernate.procedure.ParameterRegistration; import org.hibernate.procedure.ParameterRegistration;
@ -90,7 +92,7 @@ public class ProcedureOutputsImpl extends OutputsImpl implements ProcedureOutput
.getService( RefCursorSupport.class ) .getService( RefCursorSupport.class )
.getResultSet( ProcedureOutputsImpl.this.callableStatement, refCursorParam.getPosition() ); .getResultSet( ProcedureOutputsImpl.this.callableStatement, refCursorParam.getPosition() );
} }
return buildResultSetOutput( extractResults( resultSet ) ); return buildResultSetOutput( () -> extractResults( resultSet ) );
} }
} }

View File

@ -13,6 +13,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import org.hibernate.JDBCException; import org.hibernate.JDBCException;
import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.QueryParameters;
@ -201,6 +202,10 @@ public class OutputsImpl implements Outputs {
return new ResultSetOutputImpl( list ); return new ResultSetOutputImpl( list );
} }
protected Output buildResultSetOutput(Supplier<List> listSupplier) {
return new ResultSetOutputImpl( listSupplier );
}
protected Output buildUpdateCountOutput(int updateCount) { protected Output buildUpdateCountOutput(int updateCount) {
return new UpdateCountOutputImpl( updateCount ); return new UpdateCountOutputImpl( updateCount );
} }

View File

@ -6,7 +6,11 @@
*/ */
package org.hibernate.result.internal; package org.hibernate.result.internal;
import java.sql.ResultSet;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import javax.enterprise.inject.spi.Producer;
import org.hibernate.result.ResultSetOutput; import org.hibernate.result.ResultSetOutput;
@ -16,10 +20,14 @@ import org.hibernate.result.ResultSetOutput;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
class ResultSetOutputImpl implements ResultSetOutput { class ResultSetOutputImpl implements ResultSetOutput {
private final List results; private final Supplier<List> resultSetSupplier;
public ResultSetOutputImpl(List results) { public ResultSetOutputImpl(List results) {
this.results = results; this.resultSetSupplier = () -> results;
}
public ResultSetOutputImpl(Supplier<List> resultSetSupplier) {
this.resultSetSupplier = resultSetSupplier;
} }
@Override @Override
@ -30,7 +38,7 @@ class ResultSetOutputImpl implements ResultSetOutput {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List getResultList() { public List getResultList() {
return results; return resultSetSupplier.get();
} }
@Override @Override

View File

@ -40,8 +40,8 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas
@Override @Override
protected Class<?>[] getAnnotatedClasses() { protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { return new Class<?>[] {
Person.class, Person.class,
Phone.class, Phone.class,
}; };
} }
@ -54,51 +54,50 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas
try { try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override Statement statement = null;
public void execute(Connection connection) throws SQLException { try {
Statement statement = null; statement = connection.createStatement();
try { statement.executeUpdate(
statement = connection.createStatement(); "CREATE PROCEDURE sp_count_phones (" +
statement.executeUpdate( " IN personId INT, " +
"CREATE PROCEDURE sp_count_phones (" + " OUT phoneCount INT " +
" IN personId INT, " + ") " +
" OUT phoneCount INT " + "BEGIN " +
") " + " SELECT COUNT(*) INTO phoneCount " +
"BEGIN " + " FROM Phone p " +
" SELECT COUNT(*) INTO phoneCount " + " WHERE p.person_id = personId; " +
" FROM Phone p " + "END"
" WHERE p.person_id = personId; " + );
"END"
); statement.executeUpdate(
statement.executeUpdate( "CREATE PROCEDURE sp_phones(IN personId INT) " +
"CREATE PROCEDURE sp_phones(IN personId INT) " + "BEGIN " +
"BEGIN " + " SELECT * " +
" SELECT * " + " FROM Phone " +
" FROM Phone " + " WHERE person_id = personId; " +
" WHERE person_id = personId; " + "END"
"END" );
);
statement.executeUpdate( statement.executeUpdate(
"CREATE FUNCTION fn_count_phones(personId integer) " + "CREATE FUNCTION fn_count_phones(personId integer) " +
"RETURNS integer " + "RETURNS integer " +
"DETERMINISTIC " + "DETERMINISTIC " +
"READS SQL DATA " + "READS SQL DATA " +
"BEGIN " + "BEGIN " +
" DECLARE phoneCount integer; " + " DECLARE phoneCount integer; " +
" SELECT COUNT(*) INTO phoneCount " + " SELECT COUNT(*) INTO phoneCount " +
" FROM Phone p " + " FROM Phone p " +
" WHERE p.person_id = personId; " + " WHERE p.person_id = personId; " +
" RETURN phoneCount; " + " RETURN phoneCount; " +
"END" "END"
); );
} finally { } finally {
if ( statement != null ) { if ( statement != null ) {
statement.close(); statement.close();
}
} }
} }
} ); } );
} }
finally { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();
@ -140,16 +139,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas
try { try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override try (Statement statement = connection.createStatement()) {
public void execute(Connection connection) throws SQLException { statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_count_phones" );
try (Statement statement = connection.createStatement()) {
statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_count_phones" );
}
catch (SQLException ignore) {
}
} }
} ); catch (SQLException ignore) {
}
} );
} }
finally { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();
@ -161,16 +157,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas
try { try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override try (Statement statement = connection.createStatement()) {
public void execute(Connection connection) throws SQLException { statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_phones" );
try (Statement statement = connection.createStatement()) {
statement.executeUpdate( "DROP PROCEDURE IF EXISTS sp_phones" );
}
catch (SQLException ignore) {
}
} }
} ); catch (SQLException ignore) {
}
} );
} }
finally { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();
@ -182,16 +175,13 @@ public class MySQLStoredProcedureTest extends BaseEntityManagerFunctionalTestCas
try { try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override try (Statement statement = connection.createStatement()) {
public void execute(Connection connection) throws SQLException { statement.executeUpdate( "DROP FUNCTION IF EXISTS fn_count_phones" );
try (Statement statement = connection.createStatement()) {
statement.executeUpdate( "DROP FUNCTION IF EXISTS fn_count_phones" );
}
catch (SQLException ignore) {
}
} }
} ); catch (SQLException ignore) {
}
} );
} }
finally { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();

View File

@ -11,8 +11,13 @@ import java.sql.Types;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import javax.persistence.Entity;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.NamedStoredProcedureQueries;
import javax.persistence.NamedStoredProcedureQuery;
import javax.persistence.ParameterMode; import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureParameter;
import javax.persistence.StoredProcedureQuery; import javax.persistence.StoredProcedureQuery;
import org.hibernate.Session; import org.hibernate.Session;
@ -25,12 +30,17 @@ import org.hibernate.result.ResultSetOutput;
import org.hibernate.testing.FailureExpected; import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.RequiresDialect; import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.transaction.TransactionUtil;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
/** /**
@ -44,9 +54,34 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa
return new Class<?>[] { return new Class<?>[] {
Person.class, Person.class,
Phone.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 @Before
public void init() { public void init() {
EntityManager entityManager = createEntityManager(); EntityManager entityManager = createEntityManager();
@ -55,79 +90,95 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa
try { try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override Statement statement = null;
public void execute(Connection connection) throws SQLException { try {
Statement statement = null; statement = connection.createStatement();
try { statement.executeUpdate(
statement = connection.createStatement(); "CREATE OR REPLACE PROCEDURE sp_count_phones ( " +
statement.executeUpdate( " personId IN NUMBER, " +
"CREATE OR REPLACE PROCEDURE sp_count_phones ( " + " phoneCount OUT NUMBER ) " +
" personId IN NUMBER, " + "AS " +
" phoneCount OUT NUMBER ) " + "BEGIN " +
"AS " + " SELECT COUNT(*) INTO phoneCount " +
"BEGIN " + " FROM phone " +
" SELECT COUNT(*) INTO phoneCount " + " WHERE person_id = personId; " +
" FROM phone " + "END;"
" WHERE person_id = personId; " + );
"END;" statement.executeUpdate(
); "CREATE OR REPLACE PROCEDURE sp_person_phones ( " +
statement.executeUpdate( " personId IN NUMBER, " +
"CREATE OR REPLACE PROCEDURE sp_person_phones ( " + " personPhones OUT SYS_REFCURSOR ) " +
" personId IN NUMBER, " + "AS " +
" personPhones OUT SYS_REFCURSOR ) " + "BEGIN " +
"AS " + " OPEN personPhones FOR " +
"BEGIN " + " SELECT *" +
" OPEN personPhones FOR " + " FROM phone " +
" SELECT *" + " WHERE person_id = personId; " +
" FROM phone " + "END;"
" WHERE person_id = personId; " + );
"END;" statement.executeUpdate(
); "CREATE OR REPLACE FUNCTION fn_count_phones ( " +
statement.executeUpdate( " personId IN NUMBER ) " +
"CREATE OR REPLACE FUNCTION fn_count_phones ( " + " RETURN NUMBER " +
" personId IN NUMBER ) " + "IS " +
" RETURN NUMBER " + " phoneCount NUMBER; " +
"IS " + "BEGIN " +
" phoneCount NUMBER; " + " SELECT COUNT(*) INTO phoneCount " +
"BEGIN " + " FROM phone " +
" SELECT COUNT(*) INTO phoneCount " + " WHERE person_id = personId; " +
" FROM phone " + " RETURN( phoneCount ); " +
" WHERE person_id = personId; " + "END;"
" RETURN( phoneCount ); " + );
"END;" statement.executeUpdate(
); "CREATE OR REPLACE FUNCTION fn_person_and_phones ( " +
statement.executeUpdate( " personId IN NUMBER ) " +
"CREATE OR REPLACE FUNCTION fn_person_and_phones ( " + " RETURN SYS_REFCURSOR " +
" personId IN NUMBER ) " + "IS " +
" RETURN SYS_REFCURSOR " + " personAndPhones SYS_REFCURSOR; " +
"IS " + "BEGIN " +
" personAndPhones SYS_REFCURSOR; " + " OPEN personAndPhones FOR " +
"BEGIN " + " SELECT " +
" OPEN personAndPhones FOR " + " pr.id AS \"pr.id\", " +
" SELECT " + " pr.name AS \"pr.name\", " +
" pr.id AS \"pr.id\", " + " pr.nickName AS \"pr.nickName\", " +
" pr.name AS \"pr.name\", " + " pr.address AS \"pr.address\", " +
" pr.nickName AS \"pr.nickName\", " + " pr.createdOn AS \"pr.createdOn\", " +
" pr.address AS \"pr.address\", " + " pr.version AS \"pr.version\", " +
" pr.createdOn AS \"pr.createdOn\", " + " ph.id AS \"ph.id\", " +
" pr.version AS \"pr.version\", " + " ph.person_id AS \"ph.person_id\", " +
" ph.id AS \"ph.id\", " + " ph.phone_number AS \"ph.phone_number\" " +
" ph.person_id AS \"ph.person_id\", " + " FROM person pr " +
" ph.phone_number AS \"ph.phone_number\" " + " JOIN phone ph ON pr.id = ph.person_id " +
" FROM person pr " + " WHERE pr.id = personId; " +
" JOIN phone ph ON pr.id = ph.person_id " + " RETURN personAndPhones; " +
" WHERE pr.id = personId; " + "END;"
" RETURN personAndPhones; " + );
"END;" statement.executeUpdate(
); "CREATE OR REPLACE " +
} finally { "PROCEDURE singleRefCursor(p_recordset OUT SYS_REFCURSOR) AS " +
if ( statement != null ) { " BEGIN " +
statement.close(); " 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 { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();
@ -357,4 +408,54 @@ public class OracleStoredProcedureTest extends BaseEntityManagerFunctionalTestCa
entityManager.close(); 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 ) );
} );
}
} }

View File

@ -1,7 +1,7 @@
package org.hibernate.test.procedure; package org.hibernate.test.procedure;
import java.sql.CallableStatement; import java.sql.CallableStatement;
import java.sql.Connection; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement; import java.sql.Statement;
@ -10,21 +10,22 @@ import java.sql.Types;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.ParameterMode; import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureQuery; import javax.persistence.StoredProcedureQuery;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.dialect.PostgreSQL81Dialect; import org.hibernate.dialect.PostgreSQL81Dialect;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.jdbc.Work;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.RequiresDialect; import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.TestForIssue;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
/** /**
* @author Vlad Mihalcea * @author Vlad Mihalcea
@ -36,19 +37,16 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
protected Class<?>[] getAnnotatedClasses() { protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { return new Class<?>[] {
Person.class, Person.class,
Phone.class, Phone.class
}; };
} }
@Before @Before
public void init() { public void init() {
EntityManager entityManager = createEntityManager(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.getTransaction().begin(); Session session = entityManager.unwrap( Session.class );
Session session = entityManager.unwrap( Session.class );
session.doWork( new Work() { session.doWork( connection -> {
@Override
public void execute(Connection connection) throws SQLException {
Statement statement = null; Statement statement = null;
try { try {
statement = connection.createStatement(); statement = connection.createStatement();
@ -61,19 +59,13 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
statement.close(); statement.close();
} }
} }
} } );
} ); } );
entityManager.getTransaction().commit(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.close(); Session session = entityManager.unwrap( Session.class );
entityManager = createEntityManager(); session.doWork( connection -> {
entityManager.getTransaction().begin();
session = entityManager.unwrap( Session.class );
session.doWork( new Work() {
@Override
public void execute(Connection connection) throws SQLException {
Statement statement = null; Statement statement = null;
try { try {
statement = connection.createStatement(); statement = connection.createStatement();
@ -86,19 +78,32 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
statement.close(); statement.close();
} }
} }
} } );
} ); } );
entityManager.getTransaction().commit(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.close(); Session session = entityManager.unwrap( Session.class );
entityManager = createEntityManager(); session.doWork( connection -> {
entityManager.getTransaction().begin(); Statement statement = null;
session = entityManager.unwrap( Session.class ); try {
statement = connection.createStatement();
statement.executeUpdate( "DROP FUNCTION singleRefCursor(bigint)" );
}
catch (SQLException ignore) {
}
finally {
if ( statement != null ) {
statement.close();
}
}
} );
} );
session.doWork( new Work() { doInJPA( this::entityManagerFactory, entityManager -> {
@Override Session session = entityManager.unwrap( Session.class );
public void execute(Connection connection) throws SQLException {
session.doWork( connection -> {
Statement statement = null; Statement statement = null;
try { try {
statement = connection.createStatement(); statement = connection.createStatement();
@ -133,48 +138,52 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
"$BODY$ " + "$BODY$ " +
"LANGUAGE plpgsql" "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 { finally {
if ( statement != null ) { if ( statement != null ) {
statement.close(); statement.close();
} }
} }
} } );
} ); } );
entityManager.getTransaction().commit(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.close(); 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.persist( person1 );
entityManager.getTransaction().begin();
Person person1 = new Person( "John Doe" ); Phone phone1 = new Phone( "123-456-7890" );
person1.setNickName( "JD" ); phone1.setId( 1L );
person1.setAddress( "Earth" );
person1.setCreatedOn( Timestamp.from( LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ) ) );
entityManager.persist( person1 ); person1.addPhone( phone1 );
Phone phone1 = new Phone( "123-456-7890" ); Phone phone2 = new Phone( "098_765-4321" );
phone1.setId( 1L ); phone2.setId( 2L );
person1.addPhone( phone1 ); person1.addPhone( phone2 );
} );
Phone phone2 = new Phone( "098_765-4321" ); }
phone2.setId( 2L );
person1.addPhone( phone2 );
entityManager.getTransaction().commit();
entityManager.close();
}
@Test @Test
public void testStoredProcedureOutParameter() { public void testStoredProcedureOutParameter() {
EntityManager entityManager = createEntityManager(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.getTransaction().begin();
try {
StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "sp_count_phones" ); StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "sp_count_phones" );
query.registerStoredProcedureParameter( "personId", Long.class, ParameterMode.IN ); query.registerStoredProcedureParameter( "personId", Long.class, ParameterMode.IN );
query.registerStoredProcedureParameter( "phoneCount", Long.class, ParameterMode.OUT ); query.registerStoredProcedureParameter( "phoneCount", Long.class, ParameterMode.OUT );
@ -184,19 +193,12 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
query.execute(); query.execute();
Long phoneCount = (Long) query.getOutputParameterValue( "phoneCount" ); Long phoneCount = (Long) query.getOutputParameterValue( "phoneCount" );
assertEquals( Long.valueOf( 2 ), phoneCount ); assertEquals( Long.valueOf( 2 ), phoneCount );
} } );
finally {
entityManager.getTransaction().rollback();
entityManager.close();
}
} }
@Test @Test
public void testStoredProcedureRefCursor() { public void testStoredProcedureRefCursor() {
EntityManager entityManager = createEntityManager(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.getTransaction().begin();
try {
StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "fn_phones" ); StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "fn_phones" );
query.registerStoredProcedureParameter( 1, void.class, ParameterMode.REF_CURSOR ); query.registerStoredProcedureParameter( 1, void.class, ParameterMode.REF_CURSOR );
query.registerStoredProcedureParameter( 2, Long.class, ParameterMode.IN ); query.registerStoredProcedureParameter( 2, Long.class, ParameterMode.IN );
@ -205,56 +207,38 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
List<Object[]> phones = query.getResultList(); List<Object[]> phones = query.getResultList();
assertEquals( 2, phones.size() ); assertEquals( 2, phones.size() );
} } );
finally {
entityManager.getTransaction().rollback();
entityManager.close();
}
} }
@Test @Test
public void testFunctionWithJDBC() { public void testFunctionWithJDBC() {
EntityManager entityManager = createEntityManager(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.getTransaction().begin();
try {
Session session = entityManager.unwrap( Session.class ); Session session = entityManager.unwrap( Session.class );
Long phoneCount = session.doReturningWork( new ReturningWork<Long>() { Long phoneCount = session.doReturningWork( connection -> {
@Override CallableStatement function = null;
public Long execute(Connection connection) throws SQLException { try {
CallableStatement function = null; function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" );
try { function.registerOutParameter( 1, Types.BIGINT );
function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" ); function.setLong( 2, 1L );
function.registerOutParameter( 1, Types.BIGINT ); function.execute();
function.setLong( 2, 1L ); return function.getLong( 1 );
function.execute(); }
return function.getLong( 1 ); finally {
} if ( function != null ) {
finally { function.close();
if ( function != null ) {
function.close();
}
} }
} }
} ); } );
assertEquals( Long.valueOf( 2 ), phoneCount ); assertEquals( Long.valueOf( 2 ), phoneCount );
} } );
finally {
entityManager.getTransaction().rollback();
entityManager.close();
}
} }
@Test @Test
public void testFunctionWithJDBCByName() { public void testFunctionWithJDBCByName() {
EntityManager entityManager = createEntityManager(); doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.getTransaction().begin(); try {
Session session = entityManager.unwrap( Session.class );
try { Long phoneCount = session.doReturningWork( connection -> {
Session session = entityManager.unwrap( Session.class );
Long phoneCount = session.doReturningWork( new ReturningWork<Long>() {
@Override
public Long execute(Connection connection) throws SQLException {
CallableStatement function = null; CallableStatement function = null;
try { try {
function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" ); function = connection.prepareCall( "{ ? = call sp_count_phones(?) }" );
@ -268,15 +252,65 @@ public class PostgreSQLStoredProcedureTest extends BaseEntityManagerFunctionalTe
function.close(); 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 );
} }
} ); finally {
assertEquals( Long.valueOf( 2 ), phoneCount ); if ( function != null ) {
} catch (Exception e) { function.close();
assertEquals( SQLFeatureNotSupportedException.class, e.getCause().getClass() ); }
} }
finally { } )) {
entityManager.getTransaction().rollback(); while ( resultSet.next() ) {
entityManager.close(); 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 );
} );
} }
} }