HHH-16917 - Number not allowed as type for procedure query parameter
This commit is contained in:
parent
8031952d86
commit
8386e1851e
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.procedure;
|
||||||
|
|
||||||
|
import org.hibernate.HibernateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates Hibernate is unable to determine the type details for a parameter.
|
||||||
|
*
|
||||||
|
* @author Steve Ebersole
|
||||||
|
*/
|
||||||
|
public class ParameterTypeException extends HibernateException {
|
||||||
|
public ParameterTypeException(String message) {
|
||||||
|
super( message );
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,12 @@ package org.hibernate.procedure.internal;
|
||||||
import java.sql.CallableStatement;
|
import java.sql.CallableStatement;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData;
|
import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData;
|
||||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||||
|
import org.hibernate.procedure.ParameterTypeException;
|
||||||
import org.hibernate.procedure.spi.NamedCallableQueryMemento;
|
import org.hibernate.procedure.spi.NamedCallableQueryMemento;
|
||||||
import org.hibernate.procedure.spi.ParameterStrategy;
|
import org.hibernate.procedure.spi.ParameterStrategy;
|
||||||
import org.hibernate.procedure.spi.ProcedureCallImplementor;
|
import org.hibernate.procedure.spi.ProcedureCallImplementor;
|
||||||
|
@ -44,6 +46,9 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
|
||||||
private final ParameterMode mode;
|
private final ParameterMode mode;
|
||||||
private final Class<T> javaType;
|
private final Class<T> javaType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for named Query parameters
|
||||||
|
*/
|
||||||
public ProcedureParameterImpl(
|
public ProcedureParameterImpl(
|
||||||
String name,
|
String name,
|
||||||
ParameterMode mode,
|
ParameterMode mode,
|
||||||
|
@ -56,6 +61,9 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
|
||||||
this.javaType = javaType;
|
this.javaType = javaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for ordinal Query parameters
|
||||||
|
*/
|
||||||
public ProcedureParameterImpl(
|
public ProcedureParameterImpl(
|
||||||
Integer position,
|
Integer position,
|
||||||
ParameterMode mode,
|
ParameterMode mode,
|
||||||
|
@ -115,21 +123,32 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
|
||||||
int startIndex,
|
int startIndex,
|
||||||
ProcedureCallImplementor<?> procedureCall) {
|
ProcedureCallImplementor<?> procedureCall) {
|
||||||
final QueryParameterBinding<T> binding = procedureCall.getParameterBindings().getBinding( this );
|
final QueryParameterBinding<T> binding = procedureCall.getParameterBindings().getBinding( this );
|
||||||
|
final boolean isNamed = procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && this.name != null;
|
||||||
|
|
||||||
|
final BindableType<T> bindableType;
|
||||||
|
if ( getHibernateType() != null ) {
|
||||||
|
bindableType = getHibernateType();
|
||||||
|
}
|
||||||
|
else if ( binding != null ) {
|
||||||
|
//noinspection unchecked
|
||||||
|
bindableType = (BindableType<T>) binding.getBindType();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bindableType = null;
|
||||||
|
}
|
||||||
|
|
||||||
final OutputableType<T> typeToUse = (OutputableType<T>) BindingTypeHelper.INSTANCE.resolveTemporalPrecision(
|
final OutputableType<T> typeToUse = (OutputableType<T>) BindingTypeHelper.INSTANCE.resolveTemporalPrecision(
|
||||||
binding == null || binding.getExplicitTemporalPrecision() == null
|
binding == null ? null : binding.getExplicitTemporalPrecision(),
|
||||||
? null
|
bindableType,
|
||||||
: binding.getExplicitTemporalPrecision(),
|
|
||||||
getHibernateType(),
|
|
||||||
procedureCall.getSession().getFactory()
|
procedureCall.getSession().getFactory()
|
||||||
);
|
);
|
||||||
|
|
||||||
final String name;
|
final String jdbcParamName;
|
||||||
if ( procedureCall.getParameterStrategy() == ParameterStrategy.NAMED
|
if ( isNamed && canDoNameParameterBinding( typeToUse, procedureCall ) ) {
|
||||||
&& canDoNameParameterBinding( typeToUse, procedureCall ) ) {
|
jdbcParamName = this.name;
|
||||||
name = this.name;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
name = null;
|
jdbcParamName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JdbcParameterBinder parameterBinder;
|
final JdbcParameterBinder parameterBinder;
|
||||||
|
@ -138,31 +157,58 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
|
||||||
|
|
||||||
switch ( mode ) {
|
switch ( mode ) {
|
||||||
case REF_CURSOR:
|
case REF_CURSOR:
|
||||||
refCursorExtractor = new JdbcCallRefCursorExtractorImpl( name, startIndex );
|
refCursorExtractor = new JdbcCallRefCursorExtractorImpl( jdbcParamName, startIndex );
|
||||||
parameterBinder = null;
|
parameterBinder = null;
|
||||||
parameterExtractor = null;
|
parameterExtractor = null;
|
||||||
break;
|
break;
|
||||||
case IN:
|
case IN:
|
||||||
parameterBinder = getParameterBinder( typeToUse, name );
|
validateBindableType( typeToUse, startIndex );
|
||||||
|
parameterBinder = getParameterBinder( typeToUse, jdbcParamName );
|
||||||
parameterExtractor = null;
|
parameterExtractor = null;
|
||||||
refCursorExtractor = null;
|
refCursorExtractor = null;
|
||||||
break;
|
break;
|
||||||
case INOUT:
|
case INOUT:
|
||||||
parameterBinder = getParameterBinder( typeToUse, name );
|
validateBindableType( typeToUse, startIndex );
|
||||||
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), name, startIndex, typeToUse );
|
parameterBinder = getParameterBinder( typeToUse, jdbcParamName );
|
||||||
|
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), jdbcParamName, startIndex, typeToUse );
|
||||||
refCursorExtractor = null;
|
refCursorExtractor = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
validateBindableType( typeToUse, startIndex );
|
||||||
parameterBinder = null;
|
parameterBinder = null;
|
||||||
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), name, startIndex, typeToUse );
|
parameterExtractor = new JdbcCallParameterExtractorImpl<>( procedureCall.getProcedureName(), jdbcParamName, startIndex, typeToUse );
|
||||||
refCursorExtractor = null;
|
refCursorExtractor = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JdbcCallParameterRegistrationImpl( name, startIndex, mode, typeToUse, parameterBinder, parameterExtractor, refCursorExtractor );
|
return new JdbcCallParameterRegistrationImpl( jdbcParamName, startIndex, mode, typeToUse, parameterBinder, parameterExtractor, refCursorExtractor );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateBindableType(BindableType<T> bindableType, int startIndex) {
|
||||||
|
if ( bindableType == null ) {
|
||||||
|
throw new ParameterTypeException(
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"Could not determine ProcedureCall parameter bind type - %s (%s)",
|
||||||
|
this.name != null ? this.name : this.position,
|
||||||
|
startIndex
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JdbcParameterBinder getParameterBinder(BindableType<T> typeToUse, String name) {
|
private JdbcParameterBinder getParameterBinder(BindableType<T> typeToUse, String name) {
|
||||||
|
if ( typeToUse == null ) {
|
||||||
|
throw new ParameterTypeException(
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"Cannot determine the bindable type for procedure parameter %s (%s)",
|
||||||
|
this.name != null ? this.name : this.position,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( typeToUse instanceof BasicType<?> ) {
|
if ( typeToUse instanceof BasicType<?> ) {
|
||||||
if ( name == null ) {
|
if ( name == null ) {
|
||||||
return new JdbcParameterImpl( (BasicType<T>) typeToUse );
|
return new JdbcParameterImpl( (BasicType<T>) typeToUse );
|
||||||
|
@ -190,13 +236,9 @@ public class ProcedureParameterImpl<T> extends AbstractQueryParameter<T> impleme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ( typeToUse == null ) {
|
|
||||||
throw new IllegalArgumentException( "Cannot determine the bindable type for procedure parameter: " + name );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canDoNameParameterBinding(
|
private boolean canDoNameParameterBinding(
|
||||||
BindableType<?> hibernateType,
|
BindableType<?> hibernateType,
|
||||||
|
|
|
@ -11,8 +11,6 @@ import java.sql.SQLException;
|
||||||
|
|
||||||
import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport;
|
import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport;
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||||
import org.hibernate.query.BindableType;
|
|
||||||
import org.hibernate.metamodel.model.domain.BasicDomainType;
|
|
||||||
import org.hibernate.query.OutputableType;
|
import org.hibernate.query.OutputableType;
|
||||||
import org.hibernate.sql.exec.spi.JdbcCallParameterExtractor;
|
import org.hibernate.sql.exec.spi.JdbcCallParameterExtractor;
|
||||||
import org.hibernate.sql.exec.spi.JdbcCallParameterRegistration;
|
import org.hibernate.sql.exec.spi.JdbcCallParameterRegistration;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.orm.test.procedure;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess;
|
||||||
|
import org.hibernate.engine.jdbc.spi.JdbcServices;
|
||||||
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Steve Ebersole
|
||||||
|
*/
|
||||||
|
public class Helper {
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface StatementAction {
|
||||||
|
void accept(Statement statement) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void withStatement(SessionFactoryImplementor sessionFactory, StatementAction action) throws SQLException {
|
||||||
|
final JdbcConnectionAccess connectionAccess = sessionFactory.getServiceRegistry()
|
||||||
|
.getService( JdbcServices.class )
|
||||||
|
.getBootstrapJdbcConnectionAccess();
|
||||||
|
|
||||||
|
try (Connection connection = connectionAccess.obtainConnection()) {
|
||||||
|
withStatement( connection, action );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void withStatement(Connection connection, StatementAction action) throws SQLException {
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
action.accept( statement );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* 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.orm.test.procedure;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import org.hibernate.dialect.HSQLDialect;
|
||||||
|
import org.hibernate.engine.spi.SessionImplementor;
|
||||||
|
import org.hibernate.procedure.ParameterTypeException;
|
||||||
|
import org.hibernate.procedure.ProcedureCall;
|
||||||
|
|
||||||
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.Jira;
|
||||||
|
import org.hibernate.testing.orm.junit.RequiresDialect;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import jakarta.persistence.ParameterMode;
|
||||||
|
import jakarta.persistence.StoredProcedureQuery;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hibernate.type.descriptor.java.CoercionHelper.toShort;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for using unsupported Java type ({@link Number} e.g.) as a parameter type
|
||||||
|
* for {@link ProcedureCall} / {@link StoredProcedureQuery} query parameters
|
||||||
|
*
|
||||||
|
* @author Steve Ebersole
|
||||||
|
*/
|
||||||
|
@RequiresDialect( HSQLDialect.class )
|
||||||
|
@DomainModel
|
||||||
|
@SessionFactory
|
||||||
|
@Jira( "https://hibernate.atlassian.net/browse/HHH-16917" )
|
||||||
|
public class NumericParameterTypeTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParameterBaseline(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
executeQuery( Integer.class, Integer.class, 1, 2, session );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeQuery(
|
||||||
|
Class<? extends Number> inArgType,
|
||||||
|
Class<? extends Number> outArgType,
|
||||||
|
Object inArgValue,
|
||||||
|
Object expectedOutArgValue,
|
||||||
|
SessionImplementor session) {
|
||||||
|
final StoredProcedureQuery query = session.createStoredProcedureQuery( "inoutproc" );
|
||||||
|
query.registerStoredProcedureParameter( "inarg", inArgType, ParameterMode.IN );
|
||||||
|
query.registerStoredProcedureParameter( "outarg", outArgType, ParameterMode.OUT );
|
||||||
|
query.setParameter( "inarg", inArgValue );
|
||||||
|
final Object result = query.getOutputParameterValue( "outarg" );
|
||||||
|
assertThat( result ).isEqualTo( expectedOutArgValue );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInputParameter(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
// Number is fine for IN parameters because we can ultimately look at the bind value type
|
||||||
|
executeQuery( Number.class, Integer.class, 1, 2, session );
|
||||||
|
|
||||||
|
// in addition to Integer/Integer, we can also have IN parameters defined as numerous "implicit conversion" types
|
||||||
|
executeQuery( Short.class, Integer.class, 1, 2, session );
|
||||||
|
executeQuery( BigInteger.class, Integer.class, BigInteger.ONE, 2, session );
|
||||||
|
executeQuery( Double.class, Integer.class, 1.0, 2, session );
|
||||||
|
executeQuery( BigDecimal.class, Integer.class, BigDecimal.ONE, 2, session );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOutputParameter(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
try {
|
||||||
|
// Number is not fine for OUT parameters
|
||||||
|
executeQuery( Integer.class, Number.class, 1, 2, session );
|
||||||
|
fail( "Expected a ParameterTypeException" );
|
||||||
|
}
|
||||||
|
catch (ParameterTypeException expected) {
|
||||||
|
assertThat( expected.getMessage() ).contains( "outarg" );
|
||||||
|
}
|
||||||
|
|
||||||
|
// in addition to Integer/Integer, we can also have OUT parameters defined as numerous "implicit conversion" types
|
||||||
|
executeQuery( Integer.class, Short.class, 1, toShort( 2 ), session );
|
||||||
|
executeQuery( Integer.class, BigInteger.class, BigDecimal.ONE, BigInteger.valueOf( 2 ), session );
|
||||||
|
executeQuery( Integer.class, Double.class, 1, 2.0, session );
|
||||||
|
executeQuery( Integer.class, BigDecimal.class, 1, BigDecimal.valueOf( 2 ), session );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
void createProcedures(SessionFactoryScope scope) throws SQLException {
|
||||||
|
final String procedureStatement = "create procedure inoutproc (IN inarg numeric, OUT outarg numeric) " +
|
||||||
|
"begin atomic set outarg = inarg + 1;" +
|
||||||
|
"end";
|
||||||
|
|
||||||
|
Helper.withStatement( scope.getSessionFactory(), statement -> {
|
||||||
|
statement.execute( procedureStatement );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
void dropProcedures(SessionFactoryScope scope) throws SQLException {
|
||||||
|
Helper.withStatement( scope.getSessionFactory(), statement -> {
|
||||||
|
statement.execute( "drop procedure inoutproc" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue