From 5816b4ad9cb1b038c8eb2b58e3ba5f5787354e6a Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Tue, 17 May 2016 19:37:02 -0700 Subject: [PATCH] HHH-10756 : StoredProcedureQuery with OUT param fails with Oracle when using named parameters (cherry picked from commit 92dfd6993753515943ab1d8ef182b169a55bfeb0) --- .../AbstractParameterRegistrationImpl.java | 48 +++- .../StoreProcedureOutParameterByNameTest.java | 206 +++++++++++++++++ ...reProcedureOutParameterByPositionTest.java | 207 ++++++++++++++++++ 3 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByNameTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByPositionTest.java 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 d3db44f851..f3652528ba 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 @@ -264,8 +264,23 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR ); } } - for ( int i = 0; i < sqlTypesToUse.length; i++ ) { - statement.registerOutParameter( startIndex + i, sqlTypesToUse[i] ); + // TODO: sqlTypesToUse.length > 1 does not seem to have a working use case (HHH-10769). + // The idea is that an embeddable/custom type can have more than one column values + // that correspond with embeddable/custom attribute value. This does not seem to + // be working yet. For now, if sqlTypesToUse.length > 1, then register + // the out parameters by position (since we only have one name). + // This will cause a failure if there are other parameters bound by + // name and the dialect does not support "mixed" named/positional parameters; + // e.g., Oracle. + if ( sqlTypesToUse.length == 1 && + procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && + canDoNameParameterBinding() ) { + statement.registerOutParameter( getName(), sqlTypesToUse[0] ); + } + else { + for ( int i = 0; i < sqlTypesToUse.length; i++ ) { + statement.registerOutParameter( startIndex + i, sqlTypesToUse[i] ); + } } } @@ -355,12 +370,37 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR 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 + // parameter values by position (since we only have one name). + final boolean useNamed = sqlTypes.length == 1 && + procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && + canDoNameParameterBinding(); + try { if ( ProcedureParameterExtractionAware.class.isInstance( hibernateType ) ) { - return (T) ( (ProcedureParameterExtractionAware) hibernateType ).extract( statement, startIndex, session() ); + if ( useNamed ) { + return (T) ( (ProcedureParameterExtractionAware) hibernateType ).extract( + statement, + new String[] { getName() }, + session() + ); + } + else { + return (T) ( (ProcedureParameterExtractionAware) hibernateType ).extract( + statement, + startIndex, + session() + ); + } } else { - return (T) statement.getObject( startIndex ); + if ( useNamed ) { + return (T) statement.getObject( name ); + } + else { + return (T) statement.getObject( startIndex ); + } } } catch (SQLException e) { diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByNameTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByNameTest.java new file mode 100644 index 0000000000..e1c9c2beb4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByNameTest.java @@ -0,0 +1,206 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.jpa.test.procedure; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +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 javax.persistence.Table; + +import org.junit.Before; +import org.junit.Test; + +import org.hibernate.dialect.Oracle10gDialect; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Andrea Boriero + * @author Gail Badner + */ +@TestForIssue(jiraKey = "HHH-10756") +@RequiresDialect(Oracle10gDialect.class) +public class StoreProcedureOutParameterByNameTest extends BaseEntityManagerFunctionalTestCase { + EntityManagerFactory entityManagerFactory; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] {User.class}; + } + + @Before + public void startUp() { + entityManagerFactory = getOrCreateEntityManager().getEntityManagerFactory(); + + createProcedures( entityManagerFactory ); + } + + @Test + public void testOneBasicOutParameter() { + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + User user = new User(); + user.id = 1; + user.name = "aName"; + em.persist( user ); + em.getTransaction().commit(); + + em.clear(); + + try { + StoredProcedureQuery query = em.createNamedStoredProcedureQuery( "User.findNameById" ); + query.setParameter( "ID_PARAM", 1 ); + + assertEquals( "aName", query.getOutputParameterValue( "NAME_PARAM" ) ); + } + finally { + em.close(); + } + } + + @Test + public void testTwoBasicOutParameters() { + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + User user = new User(); + user.id = 1; + user.name = "aName"; + user.age = 29; + em.persist( user ); + em.getTransaction().commit(); + + em.clear(); + + try { + StoredProcedureQuery query = em.createNamedStoredProcedureQuery( "User.findNameAndAgeById" ); + query.setParameter( "ID_PARAM", 1 ); + + assertEquals( "aName", query.getOutputParameterValue( "NAME_PARAM" ) ); + assertEquals( 29, query.getOutputParameterValue( "AGE_PARAM" ) ); + } + finally { + em.close(); + } + } + + private void createProcedures(EntityManagerFactory emf) { + createProcedure( + emf, + "CREATE OR REPLACE PROCEDURE PROC_EXAMPLE_ONE_BASIC_OUT ( " + + " ID_PARAM IN NUMBER, NAME_PARAM OUT VARCHAR2 ) " + + "AS " + + "BEGIN " + + " SELECT NAME INTO NAME_PARAM FROM USERS WHERE id = ID_PARAM; " + + "END PROC_EXAMPLE_ONE_BASIC_OUT; " + ); + + createProcedure( + emf, + "CREATE OR REPLACE PROCEDURE PROC_EXAMPLE_TWO_BASIC_OUT ( " + + " ID_PARAM IN NUMBER, NAME_PARAM OUT VARCHAR2, AGE_PARAM OUT NUMBER ) " + + "AS " + + "BEGIN " + + " SELECT NAME, AGE INTO NAME_PARAM, AGE_PARAM FROM USERS WHERE id = ID_PARAM; " + + "END PROC_EXAMPLE_TWO_BASIC_OUT; " + ); + } + + private void createProcedure(EntityManagerFactory emf, String storedProc) { + final SessionFactoryImplementor sf = emf.unwrap( SessionFactoryImplementor.class ); + final JdbcConnectionAccess connectionAccess = sf.getServiceRegistry() + .getService( JdbcServices.class ) + .getBootstrapJdbcConnectionAccess(); + final Connection conn; + try { + conn = connectionAccess.obtainConnection(); + conn.setAutoCommit( false ); + + try { + Statement statement = conn.createStatement(); + + statement.execute( storedProc ); + + try { + statement.close(); + } + catch (SQLException ignore) { + fail(); + } + } + finally { + try { + conn.commit(); + } + catch (SQLException e) { + System.out.println( "Unable to commit transaction after creating creating procedures" ); + fail(); + } + + try { + connectionAccess.releaseConnection( conn ); + } + catch (SQLException ignore) { + fail(); + } + } + } + catch (SQLException e) { + throw new RuntimeException( "Unable to create stored procedures", e ); + } + } + + @NamedStoredProcedureQueries( + value = { + @NamedStoredProcedureQuery(name = "User.findNameById", + resultClasses = User.class, + procedureName = "PROC_EXAMPLE_ONE_BASIC_OUT", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "ID_PARAM", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "NAME_PARAM", type = String.class) + } + ), + @NamedStoredProcedureQuery(name = "User.findNameAndAgeById", + resultClasses = User.class, + procedureName = "PROC_EXAMPLE_TWO_BASIC_OUT", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "ID_PARAM", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "NAME_PARAM", type = String.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, name = "AGE_PARAM", type = Integer.class) + } + ) + } + ) + @Entity(name = "Message") + @Table(name = "USERS") + public static class User { + @Id + private Integer id; + + @Column(name = "NAME") + private String name; + + @Column(name = "AGE") + private int age; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByPositionTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByPositionTest.java new file mode 100644 index 0000000000..d46bf68b4f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/procedure/StoreProcedureOutParameterByPositionTest.java @@ -0,0 +1,207 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.jpa.test.procedure; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +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 javax.persistence.Table; + +import org.junit.Before; +import org.junit.Test; + +import org.hibernate.dialect.Oracle10gDialect; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Andrea Boriero + * @author Gail Badner + */ +@TestForIssue(jiraKey = "HHH-10756") +@RequiresDialect(Oracle10gDialect.class) +public class StoreProcedureOutParameterByPositionTest extends BaseEntityManagerFunctionalTestCase { + EntityManagerFactory entityManagerFactory; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] {User.class}; + } + + @Before + public void startUp() { + entityManagerFactory = getOrCreateEntityManager().getEntityManagerFactory(); + + createProcedures( entityManagerFactory ); + } + + @Test + public void testOneBasicOutParameter() { + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + User user = new User(); + user.id = 1; + user.name = "aName"; + em.persist( user ); + em.getTransaction().commit(); + + em.clear(); + + try { + StoredProcedureQuery query = em.createNamedStoredProcedureQuery( "User.findNameById" ); + query.setParameter( 1, 1 ); + + assertEquals( "aName", query.getOutputParameterValue( 2 ) ); + } + finally { + em.close(); + } + } + + @Test + public void testTwoBasicOutParameters() { + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + User user = new User(); + user.id = 1; + user.name = "aName"; + user.age = 29; + em.persist( user ); + em.getTransaction().commit(); + + em.clear(); + + try { + StoredProcedureQuery query = em.createNamedStoredProcedureQuery( "User.findNameAndAgeById" ); + query.setParameter( 1, 1 ); + + assertEquals( "aName", query.getOutputParameterValue( 2 ) ); + assertEquals( 29, query.getOutputParameterValue( 3 ) ); + } + finally { + em.close(); + } + } + + private void createProcedures(EntityManagerFactory emf) { + createProcedure( + emf, + "CREATE OR REPLACE PROCEDURE PROC_EXAMPLE_ONE_BASIC_OUT ( " + + " ID_PARAM IN NUMBER, NAME_PARAM OUT VARCHAR2 ) " + + "AS " + + "BEGIN " + + " SELECT NAME INTO NAME_PARAM FROM USERS WHERE id = ID_PARAM; " + + "END PROC_EXAMPLE; " + ); + + createProcedure( + emf, + "CREATE OR REPLACE PROCEDURE PROC_EXAMPLE_TWO_BASIC_OUT ( " + + " ID_PARAM IN NUMBER, NAME_PARAM OUT VARCHAR2, AGE_PARAM OUT NUMBER ) " + + "AS " + + "BEGIN " + + " SELECT NAME, AGE INTO NAME_PARAM, AGE_PARAM FROM USERS WHERE id = ID_PARAM; " + + "END PROC_EXAMPLE_TWO_BASIC_OUT; " + ); + } + + private void createProcedure(EntityManagerFactory emf, String storedProc) { + final SessionFactoryImplementor sf = emf.unwrap( SessionFactoryImplementor.class ); + final JdbcConnectionAccess connectionAccess = sf.getServiceRegistry() + .getService( JdbcServices.class ) + .getBootstrapJdbcConnectionAccess(); + final Connection conn; + try { + conn = connectionAccess.obtainConnection(); + conn.setAutoCommit( false ); + + try { + Statement statement = conn.createStatement(); + + statement.execute( storedProc ); + + try { + statement.close(); + } + catch (SQLException ignore) { + fail(); + } + } + finally { + try { + conn.commit(); + } + catch (SQLException e) { + System.out.println( "Unable to commit transaction after creating creating procedures" ); + fail(); + } + + try { + connectionAccess.releaseConnection( conn ); + } + catch (SQLException ignore) { + fail(); + } + } + } + catch (SQLException e) { + throw new RuntimeException( "Unable to create stored procedures", e ); + } + } + + @NamedStoredProcedureQueries( + value = { + @NamedStoredProcedureQuery(name = "User.findNameById", + resultClasses = User.class, + procedureName = "PROC_EXAMPLE" + , + parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, type = String.class) + } + ), + @NamedStoredProcedureQuery(name = "User.findNameAndAgeById", + resultClasses = User.class, + procedureName = "PROC_EXAMPLE_TWO_BASIC_OUT", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, type = String.class), + @StoredProcedureParameter(mode = ParameterMode.OUT, type = Integer.class) + } + ) + } + ) + @Entity(name = "Message") + @Table(name = "USERS") + public static class User { + @Id + private Integer id; + + @Column(name = "NAME") + private String name; + + @Column(name = "AGE") + private int age; + } +}