From c02ce35aa0a466042153bf0dec2b8c1cd853f888 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 16 Feb 2022 14:21:47 +0100 Subject: [PATCH] Implement on the fly callable named native query to stored procedure translation --- .../source/internal/hbm/NamedQueryBinder.java | 32 ++- .../cfg/annotations/QueryBinder.java | 202 +++++++++++++++++- .../orm/test/sql/hand/custom/orm.xml | 48 +++++ migration-guide.adoc | 42 +++- 4 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/orm.xml diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/NamedQueryBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/NamedQueryBinder.java index 8c1372d989..b988b2a96f 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/NamedQueryBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/NamedQueryBinder.java @@ -7,8 +7,10 @@ package org.hibernate.boot.model.source.internal.hbm; import java.util.Locale; + import jakarta.xml.bind.JAXBElement; +import org.hibernate.boot.MappingException; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmNamedNativeQueryType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmNamedQueryType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmNativeQueryCollectionLoadReturnType; @@ -20,6 +22,8 @@ import org.hibernate.boot.query.ImplicitHbmResultSetMappingDescriptorBuilder; import org.hibernate.boot.query.NamedHqlQueryDefinition; import org.hibernate.boot.query.NamedNativeQueryDefinitionBuilder; +import org.hibernate.boot.query.NamedProcedureCallDefinition; +import org.hibernate.cfg.annotations.QueryBinder; import org.hibernate.internal.log.DeprecationLogger; import org.hibernate.internal.util.StringHelper; @@ -160,7 +164,33 @@ public static void processNamedNativeQuery( builder.setResultSetMappingName( implicitResultSetMappingBuilder.getRegistrationName() ); } - context.getMetadataCollector().addNamedNativeQuery( builder.build() ); + if ( namedQueryBinding.isCallable() ) { + final NamedProcedureCallDefinition definition = QueryBinder.createStoredProcedure( + builder, context, + () -> illegalCallSyntax( context, namedQueryBinding, builder.getSqlString() ) + ); + context.getMetadataCollector().addNamedProcedureCallDefinition( definition ); + DeprecationLogger.DEPRECATION_LOGGER.warn( + "Marking named native queries as callable is deprecated; use `` instead." + ); + } + else { + context.getMetadataCollector().addNamedNativeQuery( builder.build() ); + } + } + + private static MappingException illegalCallSyntax( + HbmLocalMetadataBuildingContext context, + JaxbHbmNamedNativeQueryType namedQueryBinding, + String sqlString) { + return new MappingException( + String.format( + "Callable named native query [%s] doesn't use the JDBC call syntax: %s", + namedQueryBinding.getName(), + sqlString + ), + context.getOrigin() + ); } private static boolean processNamedQueryContentItem( diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java index 2924685265..4272100a57 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java @@ -6,21 +6,33 @@ */ package org.hibernate.cfg.annotations; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.CacheMode; import org.hibernate.FlushMode; import org.hibernate.annotations.CacheModeType; import org.hibernate.annotations.FlushModeType; +import org.hibernate.annotations.common.annotationfactory.AnnotationDescriptor; +import org.hibernate.annotations.common.annotationfactory.AnnotationFactory; import org.hibernate.boot.internal.NamedHqlQueryDefinitionImpl; import org.hibernate.boot.internal.NamedProcedureCallDefinitionImpl; import org.hibernate.boot.query.NamedHqlQueryDefinition; import org.hibernate.boot.query.NamedNativeQueryDefinition; import org.hibernate.boot.query.NamedNativeQueryDefinitionBuilder; +import org.hibernate.boot.query.NamedProcedureCallDefinition; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.cfg.BinderHelper; import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.log.DeprecationLogger; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.jpa.HibernateHints; +import org.hibernate.query.sql.internal.ParameterParser; +import org.hibernate.query.sql.spi.ParameterRecognizer; +import org.hibernate.type.BasicType; import org.jboss.logging.Logger; @@ -29,8 +41,11 @@ import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.NamedStoredProcedureQuery; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.QueryHint; import jakarta.persistence.SqlResultSetMapping; import jakarta.persistence.SqlResultSetMappings; +import jakarta.persistence.StoredProcedureParameter; /** * Query binder @@ -166,16 +181,116 @@ public static void bindNativeQuery( .setFetchSize( queryAnn.fetchSize() < 0 ? null : queryAnn.fetchSize() ) .setFlushMode( getFlushMode( queryAnn.flushMode() ) ) .setReadOnly( queryAnn.readOnly() ) + .setQuerySpaces( CollectionHelper.setOf( queryAnn.querySpaces() ) ) .setComment( BinderHelper.getAnnotationValueStringOrNull( queryAnn.comment() ) ); - final NamedNativeQueryDefinition queryDefinition = builder.build(); + if ( queryAnn.callable() ) { + final NamedProcedureCallDefinition definition = createStoredProcedure( + builder, context, + () -> illegalCallSyntax( + queryAnn, + queryAnn.query() + ) + ); + context.getMetadataCollector().addNamedProcedureCallDefinition( definition ); + DeprecationLogger.DEPRECATION_LOGGER.warn( + "Marking named native queries as callable is no longer supported; use `@jakarta.persistence.NamedStoredProcedureQuery` instead. Ignoring." + ); + } + else { + final NamedNativeQueryDefinition queryDefinition = builder.build(); - if ( LOG.isDebugEnabled() ) { - LOG.debugf( "Binding named native query: %s => %s", queryDefinition.getRegistrationName(), queryDefinition.getSqlQueryString() ); + if ( LOG.isDebugEnabled() ) { + LOG.debugf( + "Binding named native query: %s => %s", + queryDefinition.getRegistrationName(), + queryDefinition.getSqlQueryString() + ); + } + + context.getMetadataCollector().addNamedNativeQuery( queryDefinition ); } - context.getMetadataCollector().addNamedNativeQuery( queryDefinition ); + } + public static NamedProcedureCallDefinition createStoredProcedure( + NamedNativeQueryDefinitionBuilder builder, + MetadataBuildingContext context, + Supplier exceptionProducer) { + List storedProcedureParameters = new ArrayList<>(); + List queryHints = new ArrayList<>(); + List parameterNames = new ArrayList<>(); + final String sqlString = builder.getSqlString().trim(); + if ( !sqlString.startsWith( "{" ) || !sqlString.endsWith( "}" ) ) { + throw exceptionProducer.get(); + } + final String procedureName = QueryBinder.parseJdbcCall( + sqlString, + parameterNames, + exceptionProducer + ); + + AnnotationDescriptor ann = new AnnotationDescriptor( NamedStoredProcedureQuery.class ); + ann.setValue( "name", builder.getName() ); + ann.setValue( "procedureName", procedureName ); + + for ( String parameterName : parameterNames ) { + AnnotationDescriptor parameterDescriptor = new AnnotationDescriptor( StoredProcedureParameter.class ); + parameterDescriptor.setValue( "name", parameterName ); + parameterDescriptor.setValue( "mode", ParameterMode.IN ); + final String typeName = builder.getParameterTypes().get( parameterName ); + if ( typeName == null ) { + parameterDescriptor.setValue( "type", Object.class ); + } + else { + final BasicType registeredType = context.getBootstrapContext() + .getTypeConfiguration() + .getBasicTypeRegistry() + .getRegisteredType( typeName ); + parameterDescriptor.setValue( "type", registeredType.getJavaType() ); + } + storedProcedureParameters.add( AnnotationFactory.create( parameterDescriptor ) ); + } + ann.setValue( + "parameters", + storedProcedureParameters.toArray( new StoredProcedureParameter[storedProcedureParameters.size()] ) + ); + + if ( builder.getResultSetMappingName() != null ) { + ann.setValue( "resultSetMappings", new String[]{ builder.getResultSetMappingName() } ); + } + else { + ann.setValue( "resultSetMappings", new String[0] ); + } + + if ( builder.getResultSetMappingClassName() != null ) { + ann.setValue( + "resultClasses", + new Class[] { + context.getBootstrapContext() + .getClassLoaderAccess().classForName( builder.getResultSetMappingClassName() ) + } + ); + } + else { + ann.setValue( "resultClasses", new Class[0] ); + } + + if ( builder.getQuerySpaces() != null ) { + AnnotationDescriptor hintDescriptor = new AnnotationDescriptor( QueryHint.class ); + hintDescriptor.setValue( "name", HibernateHints.HINT_NATIVE_SPACES ); + hintDescriptor.setValue( "value", String.join( " ", builder.getQuerySpaces() ) ); + queryHints.add( AnnotationFactory.create( hintDescriptor ) ); + } + + AnnotationDescriptor hintDescriptor2 = new AnnotationDescriptor( QueryHint.class ); + hintDescriptor2.setValue( "name", HibernateHints.HINT_CALLABLE_FUNCTION ); + hintDescriptor2.setValue( "value", "true" ); + queryHints.add( AnnotationFactory.create( hintDescriptor2 ) ); + + ann.setValue( "hints", queryHints.toArray( new QueryHint[queryHints.size()] ) ); + + return new NamedProcedureCallDefinitionImpl( AnnotationFactory.create( ann ) ); } public static void bindQueries(NamedQueries queriesAnn, MetadataBuildingContext context, boolean isDefault) { @@ -349,5 +464,84 @@ public static void bindSqlResultSetMapping( context.getMetadataCollector().addSecondPass( new ResultsetMappingSecondPass( ann, context, isDefault ) ); } + public static String parseJdbcCall( + String sqlString, + List parameterNames, + Supplier exceptionProducer) { + String procedureName = null; + int index = skipWhitespace( sqlString, 1 ); + // Parse the out param `?=` part + if ( sqlString.charAt( index ) == '?' ) { + index++; + index = skipWhitespace( sqlString, index ); + if ( sqlString.charAt( index ) != '=' ) { + throw exceptionProducer.get(); + } + index++; + index = skipWhitespace( sqlString, index ); + } + // Parse the call keyword + if ( !sqlString.regionMatches( true, index, "call", 0, 4 ) ) { + throw exceptionProducer.get(); + } + index += 4; + index = skipWhitespace( sqlString, index ); + // Parse the procedure name + final int procedureStart = index; + for ( ; index < sqlString.length(); index++ ) { + final char c = sqlString.charAt( index ); + if ( c == '(' || Character.isWhitespace( c ) ) { + procedureName = sqlString.substring( procedureStart, index ); + break; + } + } + index = skipWhitespace( sqlString, index ); + ParameterParser.parse( + sqlString.substring( index, sqlString.length() - 1 ), + new ParameterRecognizer() { + @Override + public void ordinalParameter(int sourcePosition) { + parameterNames.add( "" ); + } + + @Override + public void namedParameter(String name, int sourcePosition) { + parameterNames.add( name ); + } + + @Override + public void jpaPositionalParameter(int label, int sourcePosition) { + parameterNames.add( "" ); + } + + @Override + public void other(char character) { + } + } + ); + return procedureName; + } + + private static int skipWhitespace(String sqlString, int i) { + while ( i < sqlString.length() ) { + if ( !Character.isWhitespace( sqlString.charAt( i ) ) ) { + break; + } + i++; + } + return i; + } + + private static AnnotationException illegalCallSyntax( + org.hibernate.annotations.NamedNativeQuery queryAnn, + String sqlString) { + return new AnnotationException( + String.format( + "Callable named native query [%s] doesn't use the JDBC call syntax: %s", + queryAnn.name(), + sqlString + ) + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/orm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/orm.xml new file mode 100644 index 0000000000..674fa11358 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/orm.xml @@ -0,0 +1,48 @@ + + + + + + simpleScalar + + + + + + paramhandling + + + + selectAllEmployments + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/migration-guide.adoc b/migration-guide.adoc index 5230e88150..723ecfff2a 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -474,26 +474,62 @@ should be changed to use `@NamedStoredProcedureQuery` instead - @NamedStoredProcedureQuery( name = "personAndPhones", procedureName = "fn_person_and_phones", - resultSetMapping = "personWithPhonesResultMapping", + resultSetMappings = "personWithPhonesResultMapping", hints = @QueryHint(name = "org.hibernate.callableFunction", value = "true"), parameters = @StoredProcedureParameter(type = Long.class) ) ``` +Callable named native queries in hbm.xml files should be migrated to the orm.xml version. + +E.g., the following `` - + +``` + + + + { ? = call simpleScalar(:number) } + + +... + +final List results = entityManager + .createNamedQuery("simpleScalar" ) + .setParameter( 1, 1L ) + .getResultList(); +``` + +should be changed to use `` instead - + +```xml + + + simpleScalar + + + + + + +``` + +TIP: To ease the migration, `` and `@NamedNativeQuery(callable = true)` queries +will be translated and registered as named stored procedure in 6.0, but future versions will drop this automatic translation. + Either `org.hibernate.procedure.ProcedureCall` or `jakarta.persistence.StoredProcedureQuery` can be used to execute the named query - ``` // Use StoredProcedureQuery final List personAndPhones = entityManager - .createNamedStoredProcedureQuery( "personAndPhones" ) + .createNamedStoredProcedureQuery( "simpleScalar" ) .setParameter( 1, 1L ) .getResultList(); // Use ProcedureCall final List personAndPhones = entityManager .unwrap( Session.class ) - .getNamedProcedureCall( "personAndPhones" ) + .getNamedProcedureCall( "simpleScalar" ) .setParameter( 1, 1L ) .getResultList(); ```