diff --git a/hibernate-core/src/main/java/org/hibernate/engine/query/internal/NativeQueryInterpreterStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/query/internal/NativeQueryInterpreterStandardImpl.java index fc3cc9d6d5ad..88120b3ff4c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/query/internal/NativeQueryInterpreterStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/query/internal/NativeQueryInterpreterStandardImpl.java @@ -24,8 +24,8 @@ public class NativeQueryInterpreterStandardImpl implements NativeQueryInterprete public static final NativeQueryInterpreterStandardImpl NATIVE_QUERY_INTERPRETER = new NativeQueryInterpreterStandardImpl(); @Override - public void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer) { - ParameterParser.parse( nativeQuery, recognizer ); + public void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer, char namedParamPrefix, char ordinalParamPrefix) { + ParameterParser.parse( nativeQuery, recognizer, namedParamPrefix, ordinalParamPrefix ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeQueryInterpreter.java b/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeQueryInterpreter.java index a45cbe564254..6129a3a44076 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeQueryInterpreter.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeQueryInterpreter.java @@ -9,6 +9,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sql.internal.NativeSelectQueryPlanImpl; +import org.hibernate.query.sql.internal.ParameterParser; import org.hibernate.query.sql.spi.NativeSelectQueryDefinition; import org.hibernate.query.sql.spi.NativeSelectQueryPlan; import org.hibernate.query.sql.spi.ParameterRecognizer; @@ -30,7 +31,15 @@ public interface NativeQueryInterpreter extends Service { * @param nativeQuery The query to recognize parameters in * @param recognizer The recognizer to call */ - void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer); + void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer, char namedParamPrefix, char ordinalParamPrefix); + + /** + * @deprecated use {@link #recognizeParameters(String, ParameterRecognizer, char, char)} + */ + @Deprecated + default void recognizeParameters(String nativeQuery, ParameterRecognizer recognizer) { + recognizeParameters( nativeQuery, recognizer, ParameterParser.NAMED_PARAM_PREFIX, ParameterParser.ORDINAL_PARAM_PREFIX ); + } /** * Creates a new query plan for the passed native query definition diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/HibernateHints.java b/hibernate-core/src/main/java/org/hibernate/jpa/HibernateHints.java index c6948bf8ec00..e32090854112 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/HibernateHints.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/HibernateHints.java @@ -156,4 +156,18 @@ public interface HibernateHints { * to a function rather than a call to a procedure. */ String HINT_CALLABLE_FUNCTION = "org.hibernate.callableFunction"; + + /** + * The prefix character used to recognize a named parameter. + * + * @see org.hibernate.query.NativeQuery#setParameterEscapes(char, char) + */ + String HINT_NAMED_PARAMETER_PREFIX = "org.hibernate.namedParameterPrefix"; + + /** + * The prefix character used to recognize an ordinal parameter. + * + * @see org.hibernate.query.NativeQuery#setParameterEscapes(char, char) + */ + String HINT_ORDINAL_PARAMETER_PREFIX = "org.hibernate.ordinalParameterPrefix"; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java b/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java index 6e2ac20e5cf9..91bd711a0b27 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java @@ -626,6 +626,18 @@ interface FetchReturn extends ResultNode { @Override NativeQuery setFirstResult(int startPosition); + /** + * Set the prefix characters used to recognize named and ordinal parameters. + * By default, named parameters are of form {@code :name}, and ordinal + * parameters are of form {@code ?n}. + * + * @param namedParamPrefix the prefix for named parameters + * @param ordinalParamPrefix the prefix for ordinal parameters + * + * @since 6.3 + */ + NativeQuery setParameterEscapes(char namedParamPrefix, char ordinalParamPrefix); + @Override NativeQuery setHint(String hintName, Object value); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index ec52a8db1fa3..8d55facbe34d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -102,7 +102,9 @@ import jakarta.persistence.Tuple; import jakarta.persistence.metamodel.SingularAttribute; +import static org.hibernate.jpa.HibernateHints.HINT_NAMED_PARAMETER_PREFIX; import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; +import static org.hibernate.jpa.HibernateHints.HINT_ORDINAL_PARAMETER_PREFIX; import static org.hibernate.query.results.Builders.resultClassBuilder; /** @@ -111,11 +113,11 @@ public class NativeQueryImpl extends AbstractQuery implements NativeQueryImplementor, DomainQueryExecutionContext, ResultSetMappingResolutionContext { - private final String sqlString; + private String sqlString; private final String originalSqlString; - private final ParameterMetadataImplementor parameterMetadata; - private final List parameterOccurrences; - private final QueryParameterBindings parameterBindings; + private ParameterMetadataImplementor parameterMetadata; + private List parameterOccurrences; + private QueryParameterBindings parameterBindings; private final ResultSetMapping resultSetMapping; private final boolean resultMappingSuppliedToCtor; @@ -125,6 +127,8 @@ public class NativeQueryImpl private Boolean startsWithSelect; private Set querySpaces; private Callback callback; + private char namedParamPrefix = ParameterParser.NAMED_PARAM_PREFIX; + private char ordinalParamPrefix = ParameterParser.ORDINAL_PARAM_PREFIX; /** * Constructs a NativeQueryImpl given a sql query defined in the mappings. @@ -196,15 +200,8 @@ public NativeQueryImpl( this.originalSqlString = memento.getOriginalSqlString(); - final ParameterInterpretation parameterInterpretation = resolveParameterInterpretation( - originalSqlString, - session - ); + interpretParameters( 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(); @@ -341,18 +338,23 @@ private ParameterInterpretation resolveParameterInterpretation( final QueryEngine queryEngine = sessionFactory.getQueryEngine(); final QueryInterpretationCache interpretationCache = queryEngine.getInterpretationCache(); - return interpretationCache.resolveNativeQueryParameters( + if ( namedParamPrefix != ParameterParser.NAMED_PARAM_PREFIX || ordinalParamPrefix != ParameterParser.ORDINAL_PARAM_PREFIX ) { + return createParameterInterpretation( sqlString, session ); + } + else { + return interpretationCache.resolveNativeQueryParameters( sqlString, - s -> { - final ParameterRecognizerImpl parameterRecognizer = new ParameterRecognizerImpl(); - - session.getFactory().getServiceRegistry() - .getService( NativeQueryInterpreter.class ) - .recognizeParameters( sqlString, parameterRecognizer ); - - return new ParameterInterpretationImpl( parameterRecognizer ); - } + s -> createParameterInterpretation( sqlString, session ) ); + } + } + + private ParameterInterpretationImpl createParameterInterpretation(String sqlString, SharedSessionContractImplementor session) { + final ParameterRecognizerImpl parameterRecognizer = new ParameterRecognizerImpl(); + session.getFactory().getServiceRegistry() + .getService( NativeQueryInterpreter.class ) + .recognizeParameters( sqlString, parameterRecognizer, namedParamPrefix, ordinalParamPrefix ); + return new ParameterInterpretationImpl( parameterRecognizer) ; } protected void applyOptions(NamedNativeQueryMemento memento) { @@ -378,17 +380,21 @@ public NativeQueryImpl(String sqlString, SharedSessionContractImplementor sessio 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() ); + interpretParameters( session ); this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() ); this.resultMappingSuppliedToCtor = false; } + private void interpretParameters(SharedSessionContractImplementor session) { + final ParameterInterpretation interpretation = resolveParameterInterpretation( originalSqlString, session ); + this.sqlString = interpretation.getAdjustedSqlString(); + this.parameterMetadata = interpretation.toParameterMetadata(session); + this.parameterOccurrences = interpretation.getOrderedParameterOccurrences(); + this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + } + private IllegalArgumentException buildIncompatibleException(Class resultClass, Class actualResultClass) { final String resultClassName = resultClass.getName(); final String actualResultClassName = actualResultClass.getName(); @@ -1494,14 +1500,29 @@ public NativeQueryImplementor setFirstResult(int startPosition) { return this; } - + @Override + public NativeQueryImplementor setParameterEscapes(char namedParamPrefix, char ordinalParamPrefix) { + this.namedParamPrefix = namedParamPrefix; + this.ordinalParamPrefix = ordinalParamPrefix; + interpretParameters( getSession() ); + return this; + } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Hints @Override public NativeQueryImplementor setHint(String hintName, Object value) { - super.setHint( hintName, value ); + switch ( hintName ) { + case HINT_NAMED_PARAMETER_PREFIX: + setParameterEscapes( (Character) value, ordinalParamPrefix ); + break; + case HINT_ORDINAL_PARAMETER_PREFIX: + setParameterEscapes( namedParamPrefix, (Character) value ); + break; + default: + super.setHint( hintName, value ); + } return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterParser.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterParser.java index 51030322a285..97bcdfd75d4b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterParser.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterParser.java @@ -23,6 +23,9 @@ public class ParameterParser { private static final String HQL_SEPARATORS = " \n\r\f\t,()=<>&|+-=/*'^![]#~\\"; private static final BitSet HQL_SEPARATORS_BITSET = new BitSet(); + public static final char NAMED_PARAM_PREFIX = ':'; + public static final char ORDINAL_PARAM_PREFIX = '?'; + static { for ( int i = 0; i < HQL_SEPARATORS.length(); i++ ) { HQL_SEPARATORS_BITSET.set( HQL_SEPARATORS.charAt( i ) ); @@ -35,6 +38,10 @@ public class ParameterParser { private ParameterParser() { } + public static void parse(String sqlString, ParameterRecognizer recognizer) { + parse( sqlString, recognizer, NAMED_PARAM_PREFIX, ORDINAL_PARAM_PREFIX ); + } + /** * Performs the actual parsing and tokenizing of the query string making appropriate * callbacks to the given recognizer upon recognition of the various tokens. @@ -47,7 +54,8 @@ private ParameterParser() { * @param recognizer The thing which handles recognition events. * @throws QueryException Indicates unexpected parameter conditions. */ - public static void parse(String sqlString, ParameterRecognizer recognizer) throws QueryException { + public static void parse(String sqlString, ParameterRecognizer recognizer, char namedParamPrefix, char ordinalParamPrefix) + throws QueryException { checkIsNotAFunctionCall( sqlString ); final int stringLength = sqlString.length(); @@ -125,12 +133,12 @@ else if ( '\\' == c ) { } // otherwise else { - if ( c == ':' && indx < stringLength - 1 && sqlString.charAt( indx + 1 ) == ':') { + if ( c == namedParamPrefix && indx < stringLength - 1 && sqlString.charAt( indx + 1 ) == namedParamPrefix) { // colon character has been escaped recognizer.other( c ); indx++; } - else if ( c == ':' ) { + else if ( c == namedParamPrefix) { // named parameter final int right = StringHelper.firstIndexOfChar( sqlString, HQL_SEPARATORS_BITSET, indx + 1 ); final int chopLocation = right < 0 ? sqlString.length() : right; @@ -143,7 +151,7 @@ else if ( c == ':' ) { recognizer.namedParameter( param, indx ); indx = chopLocation - 1; } - else if ( c == '?' ) { + else if ( c == ordinalParamPrefix) { // could be either a positional or JPA-style ordinal parameter if ( indx < stringLength - 1 && Character.isDigit( sqlString.charAt( indx + 1 ) ) ) { // a peek ahead showed this as a JPA-positional parameter diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java index 65afbf6e7dd7..0c29d0b0d491 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NativeQueryImplementor.java @@ -191,6 +191,9 @@ NativeQueryImplementor addJoin( @Override NativeQueryImplementor setFirstResult(int startPosition); + @Override + NativeQueryImplementor setParameterEscapes(char namedParamPrefix, char ordinalParamPrefix); + @Override NativeQueryImplementor addQueryHint(String hint); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixJpaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixJpaTest.java new file mode 100644 index 000000000000..51a2ab4a4679 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixJpaTest.java @@ -0,0 +1,155 @@ +/* + * 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.orm.test.jpa.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Query; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.hibernate.jpa.HibernateHints; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa( + annotatedClasses = { + NativeQueryParameterPrefixJpaTest.Game.class, + NativeQueryParameterPrefixJpaTest.Node.class + } +) +public class NativeQueryParameterPrefixJpaTest { + + private static final String[] GAME_TITLES = { "Super Mario Brothers", "Mario Kart", "F-Zero" }; + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + for ( String title : GAME_TITLES ) { + Game game = new Game( title ); + entityManager.persist( game ); + } + } + ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> entityManager.createQuery( "delete from Game" ).executeUpdate() + ); + } + + @Test + public void testNativeQueryIndexedOrdinalParameter(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Query query = entityManager.createNativeQuery( "SELECT * FROM GAME g WHERE title = @1" ); + query.setHint(HibernateHints.HINT_ORDINAL_PARAMETER_PREFIX, '@') + .setParameter( 1, "Super Mario Brothers" ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + } + ); + } + + @Test + public void testNativeQueryNamedParameter(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Query query = entityManager.createNativeQuery( "SELECT * FROM GAME g WHERE title = #t" ); + query.setHint(HibernateHints.HINT_NAMED_PARAMETER_PREFIX, '#') + .setParameter( "t", "Super Mario Brothers" ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + } + ); + } + + @Entity(name = "Game") + @Table(name = "GAME") + public static class Game { + private Long id; + private String title; + + public Game() { + } + + public Game(String title) { + this.title = title; + } + + @Id + @GeneratedValue + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @NotNull + @Size(min = 3, max = 50) + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "Node") + @Table(name = "Node") + public static class Node { + + @Id + @GeneratedValue + private Integer id; + + private String code; + + @ManyToOne(fetch = FetchType.LAZY) + private Node parent; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Node getParent() { + return parent; + } + + public void setParent(Node parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixTest.java new file mode 100644 index 000000000000..f16b1681ff73 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryParameterPrefixTest.java @@ -0,0 +1,157 @@ +/* + * 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.orm.test.jpa.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.hibernate.query.NativeQuery; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SessionFactory +@DomainModel( + annotatedClasses = { + NativeQueryParameterPrefixTest.Game.class, + NativeQueryParameterPrefixTest.Node.class + } +) +public class NativeQueryParameterPrefixTest { + + private static final String[] GAME_TITLES = { "Super Mario Brothers", "Mario Kart", "F-Zero" }; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + for ( String title : GAME_TITLES ) { + Game game = new Game( title ); + session.persist( game ); + } + } + ); + } + + @BeforeEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createNativeMutationQuery("delete GAME").executeUpdate(); + } + ); + } + + @Test + public void testNativeQueryIndexedOrdinalParameter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + NativeQuery query = session.createNativeQuery( "SELECT * FROM GAME g WHERE title = @1" ); + query.setParameterEscapes('#','@') + .setParameter( 1, "Super Mario Brothers" ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + } + ); + } + + @Test + public void testNativeQueryNamedParameter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + NativeQuery query = session.createNativeQuery( "SELECT * FROM GAME g WHERE title = #t" ); + query.setParameterEscapes('#','@') + .setParameter( "t", "Super Mario Brothers" ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + } + ); + } + + @Entity(name = "Game") + @Table(name = "GAME") + public static class Game { + private Long id; + private String title; + + public Game() { + } + + public Game(String title) { + this.title = title; + } + + @Id + @GeneratedValue + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @NotNull + @Size(min = 3, max = 50) + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "Node") + @Table(name = "Node") + public static class Node { + + @Id + @GeneratedValue + private Integer id; + + private String code; + + @ManyToOne(fetch = FetchType.LAZY) + private Node parent; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Node getParent() { + return parent; + } + + public void setParent(Node parent) { + this.parent = parent; + } + } +}