From a660996d4ea5500ba44f607fc68c2c7b5a3d9659 Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Thu, 21 Apr 2016 13:54:05 -0700 Subject: [PATCH] HHH-9548 : Backport HHH-9548 to globally enable passing null as stored procedure parameter to Hibernate 5.0 --- .../org/hibernate/cfg/AvailableSettings.java | 9 + .../AbstractParameterRegistrationImpl.java | 41 ++- ...dProcedureGlobalParamsNullPassingTest.java | 241 ++++++++++++++++++ .../sql/storedproc/StoredProcedureTest.java | 50 ++++ 4 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureGlobalParamsNullPassingTest.java 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 134af11ca1..b0f1d8ace1 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -984,4 +984,13 @@ public interface AvailableSettings { */ String AUTO_SESSION_EVENTS_LISTENER = "hibernate.session.events.auto"; + /** + * Global setting for whether NULL parameter bindings should be passed to database + * procedure/function calls as part of {@link org.hibernate.procedure.ProcedureCall} + * handling. By default, Hibernate will not pass the NULL, the intention being to allow + * any default argument values to be applied. + *

+ * Values are {@code true} (pass the NULLs) or {@code false} (do not pass the NULLs). + */ + String PROCEDURE_NULL_PARAM_PASSING = "hibernate.proc.param_null_passing"; } 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 d287a58c12..d3db44f851 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 @@ -13,6 +13,7 @@ import java.util.Date; import javax.persistence.ParameterMode; import javax.persistence.TemporalType; +import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport; import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; @@ -30,6 +31,9 @@ import org.hibernate.type.Type; import org.jboss.logging.Logger; +import static org.hibernate.cfg.AvailableSettings.PROCEDURE_NULL_PARAM_PASSING; +import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN; + /** * Abstract implementation of ParameterRegistration/ParameterRegistrationImplementor * @@ -45,6 +49,7 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR private final ParameterMode mode; private final Class type; + private final boolean passNulls; private ParameterBindImpl bind; @@ -110,6 +115,11 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR this.mode = mode; this.type = type; + this.passNulls = procedureCall.getSession().getFactory() + .getServiceRegistry() + .getService( ConfigurationService.class ) + .getSetting( PROCEDURE_NULL_PARAM_PASSING, BOOLEAN, false ); + if ( mode == ParameterMode.REF_CURSOR ) { return; } @@ -261,17 +271,28 @@ public abstract class AbstractParameterRegistrationImpl implements ParameterR if ( mode == ParameterMode.INOUT || mode == ParameterMode.IN ) { if ( bind == null || bind.getValue() == null ) { - // the user did not bind a value to the parameter being processed. That might be ok *if* the - // procedure as defined in the database defines a default value for that parameter. + // the user did not bind a value to the parameter being processed. This is the condition + // defined by `passNulls` and that value controls what happens here. If `passNulls` is + // {@code true} we will bind the NULL value into the statement; if `passNulls` is + // {@code false} we will not. + // // Unfortunately there is not a way to reliably know through JDBC metadata whether a procedure - // parameter defines a default value. So we simply allow the procedure execution to happen - // assuming that the database will complain appropriately if not setting the given parameter - // bind value is an error. - log.debugf( - "Stored procedure [%s] IN/INOUT parameter [%s] not bound; assuming procedure defines default value", - procedureCall.getProcedureName(), - this - ); + // parameter defines a default value. Deferring to that information would be the best option + if ( passNulls ) { + log.debugf( + "Stored procedure [%s] IN/INOUT parameter [%s] not bound and `passNulls` was set to true; binding NULL", + procedureCall.getProcedureName(), + this + ); + typeToUse.nullSafeSet( statement, null, startIndex, session() ); + } + else { + log.debugf( + "Stored procedure [%s] IN/INOUT parameter [%s] not bound and `passNulls` was set to false; assuming procedure defines default value", + procedureCall.getProcedureName(), + this + ); + } } else { if ( this.procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && canDoNameParameterBinding()) { diff --git a/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureGlobalParamsNullPassingTest.java b/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureGlobalParamsNullPassingTest.java new file mode 100644 index 0000000000..9b4be71498 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureGlobalParamsNullPassingTest.java @@ -0,0 +1,241 @@ +/* + * 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.test.sql.storedproc; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedStoredProcedureQueries; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.QueryHint; +import javax.persistence.StoredProcedureParameter; + +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.procedure.ProcedureCall; + +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * @author Steve Ebersole + */ +@RequiresDialect( H2Dialect.class ) +public class StoredProcedureGlobalParamsNullPassingTest extends BaseCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { MyEntity.class }; + } + + @Override + protected void configure(Configuration configuration) { + super.configure( configuration ); + configuration.setProperty( AvailableSettings.PROCEDURE_NULL_PARAM_PASSING, "true" ); + configuration.addAuxiliaryDatabaseObject( + new AuxiliaryDatabaseObject() { + @Override + public String getExportIdentifier() { + return "function:findOneUser"; + } + + @Override + public boolean appliesToDialect(Dialect dialect) { + return H2Dialect.class.isInstance( dialect ); + } + + @Override + public boolean beforeTablesOnCreation() { + return false; + } + + @Override + public String[] sqlCreateStrings(Dialect dialect) { + return new String[] { + "CREATE ALIAS findOneUser AS $$\n" + + "import org.h2.tools.SimpleResultSet;\n" + + "import java.sql.*;\n" + + "@CODE\n" + + "ResultSet findOneUser() {\n" + + " SimpleResultSet rs = new SimpleResultSet();\n" + + " rs.addColumn(\"ID\", Types.INTEGER, 10, 0);\n" + + " rs.addColumn(\"NAME\", Types.VARCHAR, 255, 0);\n" + + " rs.addRow(1, \"Steve\");\n" + + " return rs;\n" + + "}\n" + + "$$" + }; + } + + @Override + public String[] sqlDropStrings(Dialect dialect) { + return new String[] { + "DROP ALIAS findUser IF EXISTS" + }; + } + } + ); + + configuration.addAuxiliaryDatabaseObject( + new AuxiliaryDatabaseObject() { + @Override + public String getExportIdentifier() { + return "function:findUsers"; + } + + @Override + public boolean appliesToDialect(Dialect dialect) { + return H2Dialect.class.isInstance( dialect ); + } + + @Override + public boolean beforeTablesOnCreation() { + return false; + } + + @Override + public String[] sqlCreateStrings(Dialect dialect) { + return new String[] { + "CREATE ALIAS findUsers AS $$\n" + + "import org.h2.tools.SimpleResultSet;\n" + + "import java.sql.*;\n" + + "@CODE\n" + + "ResultSet findUsers() {\n" + + " SimpleResultSet rs = new SimpleResultSet();\n" + + " rs.addColumn(\"ID\", Types.INTEGER, 10, 0);\n" + + " rs.addColumn(\"NAME\", Types.VARCHAR, 255, 0);\n" + + " rs.addRow(1, \"Steve\");\n" + + " rs.addRow(2, \"John\");\n" + + " rs.addRow(3, \"Jane\");\n" + + " return rs;\n" + + "}\n" + + "$$" + }; + } + + @Override + public String[] sqlDropStrings(Dialect dialect) { + return new String[] {"DROP ALIAS findUser IF EXISTS"}; + } + } + ); + + configuration.addAuxiliaryDatabaseObject( + new AuxiliaryDatabaseObject() { + @Override + public String getExportIdentifier() { + return "function:findUserRange"; + } + + @Override + public boolean appliesToDialect(Dialect dialect) { + return H2Dialect.class.isInstance( dialect ); + } + + @Override + public boolean beforeTablesOnCreation() { + return false; + } + + @Override + public String[] sqlCreateStrings(Dialect dialect) { + return new String[] { + "CREATE ALIAS findUserRange AS $$\n" + + "import org.h2.tools.SimpleResultSet;\n" + + "import java.sql.*;\n" + + "@CODE\n" + + "ResultSet findUserRange(int start, int end) {\n" + + " SimpleResultSet rs = new SimpleResultSet();\n" + + " rs.addColumn(\"ID\", Types.INTEGER, 10, 0);\n" + + " rs.addColumn(\"NAME\", Types.VARCHAR, 255, 0);\n" + + " for ( int i = start; i < end; i++ ) {\n" + + " rs.addRow(1, \"User \" + i );\n" + + " }\n" + + " return rs;\n" + + "}\n" + + "$$" + }; + } + + @Override + public String[] sqlDropStrings(Dialect dialect) { + return new String[] {"DROP ALIAS findUserRange IF EXISTS"}; + } + } + ); + } + + @Test + public void testInParametersSetPassGlobal() { + Session session = openSession(); + session.beginTransaction(); + + // AvailableSettings.PROCEDURE_NULL_PARAM_PASSING == "true" + // so this execution should succeed + + ProcedureCall query = session.createStoredProcedureCall( "findUserRange" ); + query.registerParameter( 1, Integer.class, ParameterMode.IN ); + query.registerParameter( 2, Integer.class, ParameterMode.IN ).bindValue( 2 ); + query.getOutputs(); + +// H2 does not support named parameters +// { +// ProcedureCall query = session.createStoredProcedureCall( "findUserRange" ); +// query.registerParameter( "start", Integer.class, ParameterMode.IN ); +// query.registerParameter( "end", Integer.class, ParameterMode.IN ).bindValue( 2 ); +// try { +// query.getOutputs(); +// fail( "Expecting failure due to missing parameter bind" ); +// } +// catch (JDBCException expected) { +// } +// } + + session.getTransaction().commit(); + session.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testInParametersNullnessPassingInNamedQueriesUsingGlobal() { + Session session = openSession(); + session.beginTransaction(); + + // AvailableSettings.PROCEDURE_NULL_PARAM_PASSING == "true" + // so this execution should succeed + + // first a fixture - this execution should pass with + ProcedureCall query = session.getNamedProcedureCall( "findUserRangeNoNullPassing" ); + query.getParameterRegistration( 2 ).bindValue( 2 ); + query.getOutputs(); + + session.getTransaction().commit(); + session.close(); + } + + @Entity + @NamedStoredProcedureQuery( + name = "findUserRangeNoNullPassing", + procedureName = "findUserRange", + parameters = { + @StoredProcedureParameter( type = Integer.class ), + @StoredProcedureParameter( type = Integer.class ), + } + ) + + public static class MyEntity { + @Id + public Integer id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureTest.java index 2a679d7048..1ab4cad9e5 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/sql/storedproc/StoredProcedureTest.java @@ -7,7 +7,13 @@ package org.hibernate.test.sql.storedproc; import java.util.List; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedStoredProcedureQueries; +import javax.persistence.NamedStoredProcedureQuery; import javax.persistence.ParameterMode; +import javax.persistence.QueryHint; +import javax.persistence.StoredProcedureParameter; import org.hibernate.JDBCException; import org.hibernate.Session; @@ -34,6 +40,11 @@ import static org.junit.Assert.fail; */ @RequiresDialect( H2Dialect.class ) public class StoredProcedureTest extends BaseCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { MyEntity.class }; + } + @Override protected void configure(Configuration configuration) { super.configure( configuration ); @@ -328,4 +339,43 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase { session.getTransaction().commit(); session.close(); } + + @Test + @SuppressWarnings("unchecked") + public void testInParametersNullnessPassingInNamedQuery() { + Session session = openSession(); + session.beginTransaction(); + + // similar to #testInParametersNotSet in terms of testing + // support for specifying whether to pass NULL argument values or not. + + // first a fixture - this execution should fail + { + ProcedureCall query = session.getNamedProcedureCall( "findUserRangeNoNullPassing" ); + query.getParameterRegistration( 2 ).bindValue( 2 ); + try { + query.getOutputs(); + fail( "Expecting failure due to missing parameter bind" ); + } + catch (JDBCException ignore) { + } + } + + session.getTransaction().commit(); + session.close(); + } + + @Entity + @NamedStoredProcedureQuery( + name = "findUserRangeNoNullPassing", + procedureName = "findUserRange", + parameters = { + @StoredProcedureParameter( type = Integer.class ), + @StoredProcedureParameter( type = Integer.class ), + } + ) + public static class MyEntity { + @Id + public Integer id; + } }