HHH-17314 - Add a configuration option to ignore jdbc parameters in native queries

Signed-off-by: Jan Schatteman <jschatte@redhat.com>
This commit is contained in:
Jan Schatteman 2023-10-18 21:06:54 +02:00 committed by Jan Schatteman
parent ffee08d853
commit 763a70f633
16 changed files with 220 additions and 72 deletions

View File

@ -248,6 +248,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private TimeZone jdbcTimeZone; private TimeZone jdbcTimeZone;
private final ValueHandlingMode criteriaValueHandlingMode; private final ValueHandlingMode criteriaValueHandlingMode;
private final boolean criteriaCopyTreeEnabled; private final boolean criteriaCopyTreeEnabled;
private final boolean nativeJdbcParametersIgnored;
private final ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode; private final ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode;
// These two settings cannot be modified from the builder, // These two settings cannot be modified from the builder,
// in order to maintain consistency. // in order to maintain consistency.
@ -551,6 +552,12 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
jpaBootstrap jpaBootstrap
); );
this.nativeJdbcParametersIgnored = getBoolean(
AvailableSettings.NATIVE_IGNORE_JDBC_PARAMETERS,
configurationSettings,
false
);
// added the boolean parameter in case we want to define some form of "all" as discussed // added the boolean parameter in case we want to define some form of "all" as discussed
this.jpaCompliance = context.getJpaCompliance(); this.jpaCompliance = context.getJpaCompliance();
@ -1145,6 +1152,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
return criteriaCopyTreeEnabled; return criteriaCopyTreeEnabled;
} }
@Override
public boolean getNativeJdbcParametersIgnored() {
return nativeJdbcParametersIgnored;
}
@Override @Override
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return immutableEntityUpdateQueryHandlingMode; return immutableEntityUpdateQueryHandlingMode;

View File

@ -382,6 +382,10 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
return delegate.isCriteriaCopyTreeEnabled(); return delegate.isCriteriaCopyTreeEnabled();
} }
public boolean getNativeJdbcParametersIgnored() {
return delegate.getNativeJdbcParametersIgnored();
}
@Override @Override
public JpaCompliance getJpaCompliance() { public JpaCompliance getJpaCompliance() {
return delegate.getJpaCompliance(); return delegate.getJpaCompliance();

View File

@ -231,6 +231,13 @@ public interface SessionFactoryOptions extends QueryEngineOptions {
return false; return false;
} }
/**
* @see org.hibernate.cfg.AvailableSettings#NATIVE_IGNORE_JDBC_PARAMETERS
*/
default boolean getNativeJdbcParametersIgnored() {
return false;
}
JpaCompliance getJpaCompliance(); JpaCompliance getJpaCompliance();
boolean isFailOnPaginationOverCollectionFetchEnabled(); boolean isFailOnPaginationOverCollectionFetchEnabled();

View File

@ -6,6 +6,7 @@
*/ */
package org.hibernate.cfg; package org.hibernate.cfg;
import org.hibernate.boot.spi.SessionFactoryOptions;
import org.hibernate.query.NullPrecedence; import org.hibernate.query.NullPrecedence;
import org.hibernate.query.spi.QueryPlan; import org.hibernate.query.spi.QueryPlan;
@ -124,6 +125,16 @@ public interface QuerySettings {
*/ */
String CRITERIA_COPY_TREE = "hibernate.criteria.copy_tree"; String CRITERIA_COPY_TREE = "hibernate.criteria.copy_tree";
/**
* When set to true, indicates that ordinal parameters (represented by the '?' placeholder) in native queries will be ignored.
* <p>
* By default, this is set to false, i.e. native queries will be checked for ordinal placeholders.
* <p>
*
* @see SessionFactoryOptions#getIgnoreNativeJdbcParameters()
*/
String NATIVE_IGNORE_JDBC_PARAMETERS = "hibernate.query.native.ignore_jdbc_parameters";
/** /**
* When {@linkplain org.hibernate.query.Query#setMaxResults(int) pagination} is used * When {@linkplain org.hibernate.query.Query#setMaxResults(int) pagination} is used
* in combination with a {@code fetch join} applied to a collection or many-valued * in combination with a {@code fetch join} applied to a collection or many-valued

View File

@ -21,11 +21,17 @@ public class NativeQueryInterpreterStandardImpl implements NativeQueryInterprete
/** /**
* Singleton access * Singleton access
*/ */
public static final NativeQueryInterpreterStandardImpl NATIVE_QUERY_INTERPRETER = new NativeQueryInterpreterStandardImpl(); public static final NativeQueryInterpreterStandardImpl NATIVE_QUERY_INTERPRETER = new NativeQueryInterpreterStandardImpl( false );
private boolean nativeJdbcParametersIgnored;
public NativeQueryInterpreterStandardImpl(boolean nativeJdbcParametersIgnored) {
this.nativeJdbcParametersIgnored = nativeJdbcParametersIgnored;
}
@Override @Override
public void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer) { public void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer) {
ParameterParser.parse( nativeQuery, recognizer ); ParameterParser.parse( nativeQuery, recognizer, nativeJdbcParametersIgnored );
} }
@Override @Override

View File

@ -21,7 +21,7 @@ public class NativeQueryInterpreterInitiator implements SessionFactoryServiceIni
@Override @Override
public NativeQueryInterpreter initiateService(SessionFactoryServiceInitiatorContext context) { public NativeQueryInterpreter initiateService(SessionFactoryServiceInitiatorContext context) {
return new NativeQueryInterpreterStandardImpl(); return new NativeQueryInterpreterStandardImpl( context.getSessionFactoryOptions().getNativeJdbcParametersIgnored() );
} }
@Override @Override

View File

@ -240,6 +240,16 @@ public class SessionDelegatorBaseImpl implements SessionImplementor {
return delegate.isCriteriaCopyTreeEnabled(); return delegate.isCriteriaCopyTreeEnabled();
} }
@Override
public boolean getNativeJdbcParametersIgnored() {
return delegate.getNativeJdbcParametersIgnored();
}
@Override
public void setNativeJdbcParametersIgnored(boolean nativeJdbcParametersIgnored) {
delegate.setNativeJdbcParametersIgnored( nativeJdbcParametersIgnored );
}
@Override @Override
public boolean isOpen() { public boolean isOpen() {
return delegate.isOpen(); return delegate.isOpen();

View File

@ -381,6 +381,10 @@ public interface SharedSessionContractImplementor
boolean isCriteriaCopyTreeEnabled(); boolean isCriteriaCopyTreeEnabled();
boolean getNativeJdbcParametersIgnored();
void setNativeJdbcParametersIgnored(boolean nativeJdbcParametersIgnored);
/** /**
* Get the current {@link FlushModeType} for this session. * Get the current {@link FlushModeType} for this session.
* <p> * <p>

View File

@ -474,6 +474,16 @@ public class SharedSessionDelegatorBaseImpl implements SharedSessionContractImpl
return delegate.isCriteriaCopyTreeEnabled(); return delegate.isCriteriaCopyTreeEnabled();
} }
@Override
public boolean getNativeJdbcParametersIgnored() {
return delegate.getNativeJdbcParametersIgnored();
}
@Override
public void setNativeJdbcParametersIgnored(boolean nativeJdbcParametersIgnored) {
delegate.setNativeJdbcParametersIgnored( nativeJdbcParametersIgnored );
}
@Override @Override
public FlushModeType getFlushMode() { public FlushModeType getFlushMode() {
return delegate.getFlushMode(); return delegate.getFlushMode();

View File

@ -162,6 +162,8 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
private boolean criteriaCopyTreeEnabled; private boolean criteriaCopyTreeEnabled;
private boolean nativeJdbcParametersIgnored;
protected boolean closed; protected boolean closed;
protected boolean waitingForAutoClose; protected boolean waitingForAutoClose;
@ -183,6 +185,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
sessionEventsManager = createSessionEventsManager(options); sessionEventsManager = createSessionEventsManager(options);
entityNameResolver = new CoordinatingEntityNameResolver( factory, interceptor ); entityNameResolver = new CoordinatingEntityNameResolver( factory, interceptor );
setCriteriaCopyTreeEnabled( factory.getSessionFactoryOptions().isCriteriaCopyTreeEnabled() ); setCriteriaCopyTreeEnabled( factory.getSessionFactoryOptions().isCriteriaCopyTreeEnabled() );
setNativeJdbcParametersIgnored( factory.getSessionFactoryOptions().getNativeJdbcParametersIgnored() );
final StatementInspector statementInspector = interpret( options.getStatementInspector() ); final StatementInspector statementInspector = interpret( options.getStatementInspector() );
@ -701,6 +704,16 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
return criteriaCopyTreeEnabled; return criteriaCopyTreeEnabled;
} }
@Override
public boolean getNativeJdbcParametersIgnored() {
return nativeJdbcParametersIgnored;
}
@Override
public void setNativeJdbcParametersIgnored(boolean nativeJdbcParametersIgnored) {
this.nativeJdbcParametersIgnored = nativeJdbcParametersIgnored;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// dynamic HQL handling // dynamic HQL handling

View File

@ -173,55 +173,6 @@ public class NativeQueryImpl<R>
); );
} }
@FunctionalInterface
private interface ResultSetMappingHandler {
boolean resolveResultSetMapping(
ResultSetMapping resultSetMapping,
Consumer<String> querySpaceConsumer,
ResultSetMappingResolutionContext context);
}
private static ResultSetMapping buildResultSetMapping(
String registeredName,
boolean isDynamic,
SharedSessionContractImplementor session) {
return ResultSetMapping.resolveResultSetMapping( registeredName, isDynamic, session.getFactory() );
}
public NativeQueryImpl(
NamedNativeQueryMemento memento,
Supplier<ResultSetMapping> resultSetMappingCreator,
ResultSetMappingHandler resultSetMappingHandler,
SharedSessionContractImplementor session) {
super( session );
this.originalSqlString = memento.getOriginalSqlString();
final ParameterInterpretation parameterInterpretation = resolveParameterInterpretation(
originalSqlString,
session
);
this.sqlString = parameterInterpretation.getAdjustedSqlString();
this.parameterMetadata = parameterInterpretation.toParameterMetadata( session );
this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences();
this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() );
this.querySpaces = new HashSet<>();
this.resultSetMapping = resultSetMappingCreator.get();
//noinspection UnnecessaryLocalVariable
final boolean appliedAnyResults = resultSetMappingHandler.resolveResultSetMapping(
resultSetMapping,
querySpaces::add,
this
);
this.resultMappingSuppliedToCtor = appliedAnyResults;
applyOptions( memento );
}
/** /**
* Constructs a NativeQueryImpl given a sql query defined in the mappings. * Constructs a NativeQueryImpl given a sql query defined in the mappings.
*/ */
@ -306,6 +257,40 @@ public class NativeQueryImpl<R>
} }
public NativeQueryImpl(
NamedNativeQueryMemento memento,
Supplier<ResultSetMapping> resultSetMappingCreator,
ResultSetMappingHandler resultSetMappingHandler,
SharedSessionContractImplementor session) {
super( session );
this.originalSqlString = memento.getOriginalSqlString();
final ParameterInterpretation parameterInterpretation = resolveParameterInterpretation(
originalSqlString,
session
);
this.sqlString = parameterInterpretation.getAdjustedSqlString();
this.parameterMetadata = parameterInterpretation.toParameterMetadata( session );
this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences();
this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() );
this.querySpaces = new HashSet<>();
this.resultSetMapping = resultSetMappingCreator.get();
//noinspection UnnecessaryLocalVariable
final boolean appliedAnyResults = resultSetMappingHandler.resolveResultSetMapping(
resultSetMapping,
querySpaces::add,
this
);
this.resultMappingSuppliedToCtor = appliedAnyResults;
applyOptions( memento );
}
public NativeQueryImpl( public NativeQueryImpl(
String sqlString, String sqlString,
NamedResultSetMappingMemento resultSetMappingMemento, NamedResultSetMappingMemento resultSetMappingMemento,
@ -331,6 +316,37 @@ public class NativeQueryImpl<R>
this.resultMappingSuppliedToCtor = true; this.resultMappingSuppliedToCtor = true;
} }
public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) {
super( session );
this.querySpaces = new HashSet<>();
final ParameterInterpretation parameterInterpretation = resolveParameterInterpretation( sqlString, session );
this.originalSqlString = sqlString;
this.sqlString = parameterInterpretation.getAdjustedSqlString();
this.parameterMetadata = parameterInterpretation.toParameterMetadata( session );
this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences();
this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() );
this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() );
this.resultMappingSuppliedToCtor = false;
}
@FunctionalInterface
private interface ResultSetMappingHandler {
boolean resolveResultSetMapping(
ResultSetMapping resultSetMapping,
Consumer<String> querySpaceConsumer,
ResultSetMappingResolutionContext context);
}
private static ResultSetMapping buildResultSetMapping(
String registeredName,
boolean isDynamic,
SharedSessionContractImplementor session) {
return ResultSetMapping.resolveResultSetMapping( registeredName, isDynamic, session.getFactory() );
}
public List<ParameterOccurrence> getParameterOccurrences() { public List<ParameterOccurrence> getParameterOccurrences() {
return parameterOccurrences; return parameterOccurrences;
} }
@ -374,22 +390,6 @@ public class NativeQueryImpl<R>
// todo (6.0) : query returns // todo (6.0) : query returns
} }
public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) {
super( session );
this.querySpaces = new HashSet<>();
final ParameterInterpretation parameterInterpretation = resolveParameterInterpretation( sqlString, session );
this.originalSqlString = sqlString;
this.sqlString = parameterInterpretation.getAdjustedSqlString();
this.parameterMetadata = parameterInterpretation.toParameterMetadata( session );
this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences();
this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() );
this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() );
this.resultMappingSuppliedToCtor = false;
}
private IllegalArgumentException buildIncompatibleException(Class<?> resultClass, Class<?> actualResultClass) { private IllegalArgumentException buildIncompatibleException(Class<?> resultClass, Class<?> actualResultClass) {
final String resultClassName = resultClass.getName(); final String resultClassName = resultClass.getName();
final String actualResultClassName = actualResultClass.getName(); final String actualResultClassName = actualResultClass.getName();

View File

@ -47,9 +47,10 @@ public class ParameterParser {
* *
* @param sqlString The string to be parsed/tokenized. * @param sqlString The string to be parsed/tokenized.
* @param recognizer The thing which handles recognition events. * @param recognizer The thing which handles recognition events.
* @param nativeJdbcParametersIgnored Whether to ignore ordinal parameters in native queries or not.
* @throws QueryException Indicates unexpected parameter conditions. * @throws QueryException Indicates unexpected parameter conditions.
*/ */
public static void parse(String sqlString, ParameterRecognizer recognizer) throws QueryException { public static void parse(String sqlString, ParameterRecognizer recognizer, boolean nativeJdbcParametersIgnored) throws QueryException {
checkIsNotAFunctionCall( sqlString ); checkIsNotAFunctionCall( sqlString );
final int stringLength = sqlString.length(); final int stringLength = sqlString.length();
@ -163,9 +164,11 @@ public class ParameterParser {
} }
} }
else { else {
if ( !nativeJdbcParametersIgnored ) {
recognizer.ordinalParameter( indx ); recognizer.ordinalParameter( indx );
} }
} }
}
else { else {
recognizer.other( c ); recognizer.other( c );
} }
@ -175,6 +178,10 @@ public class ParameterParser {
recognizer.complete(); recognizer.complete();
} }
public static void parse(String sqlString, ParameterRecognizer recognizer) throws QueryException {
parse( sqlString, recognizer, false );
}
private static void checkIsNotAFunctionCall(String sqlString) { private static void checkIsNotAFunctionCall(String sqlString) {
final String trimmed = sqlString.trim(); final String trimmed = sqlString.trim();
if ( !( trimmed.startsWith( "{" ) && trimmed.endsWith( "}" ) ) ) { if ( !( trimmed.startsWith( "{" ) && trimmed.endsWith( "}" ) ) ) {

View File

@ -7,7 +7,7 @@
package org.hibernate.query.sql.spi; package org.hibernate.query.sql.spi;
/** /**
* Defines the "callback" the process of recognizing native query parameters. * Defines the "callback" process of recognizing native query parameters.
* *
* @see org.hibernate.engine.query.spi.NativeQueryInterpreter#recognizeParameters * @see org.hibernate.engine.query.spi.NativeQueryInterpreter#recognizeParameters
*/ */

View File

@ -9,12 +9,15 @@ package org.hibernate.orm.test.query.sql;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.StandardBasicTypes;
import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.StandardDomainModel;
import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
@ -34,6 +37,24 @@ public class NativeQueryParameterTests {
); );
} }
@DomainModel( standardModels = StandardDomainModel.HELPDESK )
@SessionFactory
@ServiceRegistry(
settings = {
@Setting(name = AvailableSettings.NATIVE_IGNORE_JDBC_PARAMETERS, value = "true")
}
)
@Test
public void testJdbcParameterScanningDisabled(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
// Nonsensical query just to test that the '?' is ignored
session.createNativeQuery( "select t.id, t.ticket_key, t.subject ? from Ticket t where t.ticket_key = :key" )
.setParameter( "key", "ABC-123" );
}
);
}
@Test @Test
public void testJpaStylePositionalParametersInNativeSql(SessionFactoryScope scope) { public void testJpaStylePositionalParametersInNativeSql(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction(

View File

@ -6,6 +6,8 @@
*/ */
package org.hibernate.orm.test.query.sql; package org.hibernate.orm.test.query.sql;
import org.hibernate.engine.query.ParameterRecognitionException;
import org.hibernate.engine.query.internal.NativeQueryInterpreterStandardImpl;
import org.hibernate.engine.query.spi.ParamLocationRecognizer; import org.hibernate.engine.query.spi.ParamLocationRecognizer;
import org.hibernate.query.sql.internal.ParameterParser; import org.hibernate.query.sql.internal.ParameterParser;
import org.hibernate.query.sql.spi.ParameterRecognizer; import org.hibernate.query.sql.spi.ParameterRecognizer;
@ -15,6 +17,7 @@ import org.junit.jupiter.api.Test;
import static org.hibernate.engine.query.internal.NativeQueryInterpreterStandardImpl.NATIVE_QUERY_INTERPRETER; import static org.hibernate.engine.query.internal.NativeQueryInterpreterStandardImpl.NATIVE_QUERY_INTERPRETER;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -197,4 +200,44 @@ public class ParameterParserTest {
assertEquals( 2, recognizer.getOrdinalParameterDescriptionMap().size() ); assertEquals( 2, recognizer.getOrdinalParameterDescriptionMap().size() );
} }
@Test
public void testJdbcParameterScanningEnabled() {
ParamLocationRecognizer recognizer = createRecognizer();
assertThrows(
ParameterRecognitionException.class,
() -> {
NATIVE_QUERY_INTERPRETER.recognizeParameters(
"SELECT column FROM Table WHERE column.id = :param and column.name = ?1",
recognizer
);
recognizer.validate();
},
"Mixed parameter strategies - use just one of named, positional or JPA-ordinal strategy"
);
}
@Test
public void testJdbcParameterScanningDisabled() {
ParamLocationRecognizer recognizer = createRecognizer();
// Should recognize the jpa style ordinal parameters
new NativeQueryInterpreterStandardImpl( true ).recognizeParameters(
"SELECT column FROM Table WHERE column.id = ?1 and column.name = ?2",
recognizer
);
recognizer.validate();
assertEquals( 2, recognizer.getOrdinalParameterDescriptionMap().size() );
recognizer = createRecognizer();
// Should ignore the '?'
new NativeQueryInterpreterStandardImpl( true ).recognizeParameters(
"SELECT column ? FROM Table WHERE column.id = :id",
recognizer
);
recognizer.validate();
assertTrue(recognizer.getNamedParameterDescriptionMap().containsKey("id"));
assertEquals( 0, recognizer.getOrdinalParameterDescriptionMap().size() );
}
} }

View File

@ -427,7 +427,7 @@ public abstract class MockSessionFactory
@Override @Override
public NativeQueryInterpreter getNativeQueryInterpreter() { public NativeQueryInterpreter getNativeQueryInterpreter() {
return new NativeQueryInterpreterStandardImpl(); return new NativeQueryInterpreterStandardImpl( this.getNativeJdbcParametersIgnored() );
} }
@Override @Override