HHH-10756 : StoredProcedureQuery with OUT param fails with Oracle when using named parameters

This commit is contained in:
Gail Badner 2016-05-17 19:37:02 -07:00
parent a3207ca5a8
commit 92dfd69937
3 changed files with 457 additions and 4 deletions

View File

@ -273,10 +273,25 @@ public abstract class AbstractParameterRegistrationImpl<T> implements ParameterR
); );
} }
} }
// 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++ ) { for ( int i = 0; i < sqlTypesToUse.length; i++ ) {
statement.registerOutParameter( startIndex + i, sqlTypesToUse[i] ); statement.registerOutParameter( startIndex + i, sqlTypesToUse[i] );
} }
} }
}
if ( mode == ParameterMode.INOUT || mode == ParameterMode.IN ) { if ( mode == ParameterMode.INOUT || mode == ParameterMode.IN ) {
if ( bind == null || bind.getValue() == null ) { if ( bind == null || bind.getValue() == null ) {
@ -374,14 +389,39 @@ public abstract class AbstractParameterRegistrationImpl<T> implements ParameterR
throw new ParameterMisuseException( "REF_CURSOR parameters should be accessed via results" ); 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 { try {
if ( ProcedureParameterExtractionAware.class.isInstance( hibernateType ) ) { 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 {
if ( useNamed ) {
return (T) statement.getObject( name );
} }
else { else {
return (T) statement.getObject( startIndex ); return (T) statement.getObject( startIndex );
} }
} }
}
catch (SQLException e) { catch (SQLException e) {
throw procedureCall.getSession().getFactory().getSQLExceptionHelper().convert( throw procedureCall.getSession().getFactory().getSQLExceptionHelper().convert(
e, e,

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}