All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.query.internal.QueryParameterBindingsImpl Maven / Gradle / Ivy

There is a newer version: 6.5.0.CR2
Show newest version
/*
 * 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.query.internal;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.persistence.Parameter;

import org.hibernate.HibernateException;
import org.hibernate.Incubating;
import org.hibernate.QueryException;
import org.hibernate.QueryParameterException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.query.spi.NamedParameterDescriptor;
import org.hibernate.engine.query.spi.OrdinalParameterDescriptor;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.MathHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.query.ParameterMetadata;
import org.hibernate.query.QueryParameter;
import org.hibernate.query.spi.QueryParameterBinding;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.spi.QueryParameterListBinding;
import org.hibernate.type.SerializableType;
import org.hibernate.type.Type;

/**
 * Manages the group of QueryParameterBinding for a particular query.
 *
 * @author Steve Ebersole
 * @author Chris Cranford
 */
@Incubating
public class QueryParameterBindingsImpl implements QueryParameterBindings {
	private static final CoreMessageLogger log = CoreLogging.messageLogger( QueryParameterBindingsImpl.class );

	private final SessionFactoryImplementor sessionFactory;
	private final ParameterMetadata parameterMetadata;
	private final boolean queryParametersValidationEnabled;

	private final int ordinalParamValueOffset;

	private final int jdbcStyleOrdinalCountBase;

	private Map parameterBindingMap;
	private Map parameterListBindingMap;
	private Set parametersConvertedToListBindings;

	public static QueryParameterBindingsImpl from(
			ParameterMetadata parameterMetadata,
			SessionFactoryImplementor sessionFactory,
			boolean queryParametersValidationEnabled) {
		if ( parameterMetadata == null ) {
			throw new QueryParameterException( "Query parameter metadata cannot be null" );
		}

		return new QueryParameterBindingsImpl(
				sessionFactory,
				parameterMetadata,
				queryParametersValidationEnabled
		);
	}

	private QueryParameterBindingsImpl(
			SessionFactoryImplementor sessionFactory,
			ParameterMetadata parameterMetadata,
			boolean queryParametersValidationEnabled) {
		this.sessionFactory = sessionFactory;
		this.parameterMetadata = parameterMetadata;
		this.queryParametersValidationEnabled = queryParametersValidationEnabled;

		this.parameterBindingMap = CollectionHelper.concurrentMap( parameterMetadata.getParameterCount() );

		this.jdbcStyleOrdinalCountBase = sessionFactory.getSessionFactoryOptions().jdbcStyleParamsZeroBased() ? 0 : 1;

		if ( parameterMetadata.hasPositionalParameters() ) {
			int smallestOrdinalParamLabel = Integer.MAX_VALUE;
			for ( QueryParameter queryParameter : parameterMetadata.getPositionalParameters() ) {
				if ( queryParameter.getPosition() == null ) {
					throw new HibernateException( "Non-ordinal parameter ended up in ordinal param list" );
				}

				if ( queryParameter.getPosition() < smallestOrdinalParamLabel ) {
					smallestOrdinalParamLabel = queryParameter.getPosition();
				}
			}
			ordinalParamValueOffset = smallestOrdinalParamLabel;
		}
		else {
			ordinalParamValueOffset = 0;
		}
	}

	@SuppressWarnings("WeakerAccess")
	protected QueryParameterBinding makeBinding(QueryParameter queryParameter) {
		assert ! parameterBindingMap.containsKey( queryParameter );

		if ( ! parameterMetadata.containsReference( queryParameter ) ) {
			throw new IllegalArgumentException(
					"Cannot create binding for parameter reference [" + queryParameter + "] - reference is not a parameter of this query"
			);
		}

		final QueryParameterBinding binding = makeBinding( queryParameter.getHibernateType() );
		parameterBindingMap.put( queryParameter, binding );

		return binding;
	}

	@SuppressWarnings("WeakerAccess")
	protected QueryParameterBinding makeBinding(Type bindType) {
		return new QueryParameterBindingImpl( bindType, sessionFactory, shouldValidateBindingValue() );
	}

	@SuppressWarnings({"unchecked", "WeakerAccess"})
	protected  QueryParameterListBinding makeListBinding(QueryParameter param) {
		if ( parametersConvertedToListBindings == null ) {
			parametersConvertedToListBindings = new HashSet<>();
		}

		parametersConvertedToListBindings.add( param );

		if ( parameterListBindingMap == null ) {
			parameterListBindingMap = new HashMap<>();
		}

		return parameterListBindingMap.computeIfAbsent(
				param,
				p -> new QueryParameterListBindingImpl(
						param.getHibernateType(),
						shouldValidateBindingValue()
				)
		);
	}

	@Override
	@SuppressWarnings( "unchecked" )
	public boolean isBound(QueryParameter parameter) {
		final QueryParameterBinding binding = getBinding( parameter );

		return binding.isBound();
	}

	@SuppressWarnings("unchecked")
	public  QueryParameterBinding getBinding(QueryParameter parameter) {
		QueryParameterBinding binding = parameterBindingMap.get( parameter );

		if ( binding == null ) {
			if ( ! parameterMetadata.containsReference( parameter ) ) {
				throw new IllegalArgumentException(
						"Could not resolve QueryParameter reference [" + parameter + "] to QueryParameterBinding"
				);
			}

			binding = makeBinding( parameter );
		}

		return binding;
	}

	@Override
	@SuppressWarnings("unchecked")
	public QueryParameterBinding getBinding(int position) {
		return getBinding( parameterMetadata.getQueryParameter( position ) );
	}

	@Override
	@SuppressWarnings("unchecked")
	public QueryParameterBinding getBinding(String name) {
		return getBinding( parameterMetadata.getQueryParameter( name ) );
	}

	public void verifyParametersBound(boolean reserveFirstParameter) {
		for ( QueryParameter parameter : parameterMetadata.collectAllParameters() ) {
			// check the "normal" bindings
			if ( parameterBindingMap.containsKey( parameter ) ) {
				continue;
			}

			// next check the "list" bindings
			if ( parameterListBindingMap != null
					&& parameterListBindingMap.containsKey( parameter ) ) {
				continue;
			}

			if ( parametersConvertedToListBindings != null
					&& parametersConvertedToListBindings.contains( parameter ) ) {
				continue;
			}

			if ( parameter.getName() != null ) {
				throw new QueryException( "Named parameter not bound : " + parameter.getName() );
			}
			else {
				throw new QueryException( "Ordinal parameter not bound : " + parameter.getPosition() );
			}
		}
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Collection collectBindTypes() {
		return parameterBindingMap.values()
				.stream()
				.map( QueryParameterBinding::getBindType )
				.collect( Collectors.toList() );
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Collection collectBindValues() {
		return parameterBindingMap.values()
				.stream()
				.map( QueryParameterBinding::getBindValue )
				.collect( Collectors.toList() );
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Type[] collectPositionalBindTypes() {
		return ArrayHelper.EMPTY_TYPE_ARRAY;
//		if ( ! parameterMetadata.hasPositionalParameters() ) {
//			return ArrayHelper.EMPTY_TYPE_ARRAY;
//		}
//
//		// callers expect these in ordinal order.  In a way that is natural, but at the same
//		// time long term a way to find types/values by name/position would be better
//
//		final TreeMap sortedPositionalParamBindings = getSortedPositionalParamBindingMap();
//		final List types = CollectionHelper.arrayList( sortedPositionalParamBindings.size() );
//
//		for ( Map.Entry entry : sortedPositionalParamBindings.entrySet() ) {
//			if ( entry.getKey().getPosition() == null ) {
//				continue;
//			}
//
//			Type type = entry.getValue().getBindType();
//			if ( type == null ) {
//				type = entry.getKey().getType();
//			}
//
//			if ( type == null ) {
//				log.debugf(
//						"Binding for positional-parameter [%s] did not define type, using SerializableType",
//						entry.getKey().getPosition()
//				);
//				type = SerializableType.INSTANCE;
//			}
//
//			types.add( type );
//		}
//
//		return types.toArray( new Type[ types.size() ] );
	}

	private TreeMap getSortedPositionalParamBindingMap() {
		final TreeMap map = new TreeMap<>( Comparator.comparing( Parameter::getPosition ) );

		for ( Map.Entry entry : parameterBindingMap.entrySet() ) {
			if ( entry.getKey().getPosition() == null ) {
				continue;
			}

			map.put( entry.getKey(), entry.getValue() );
		}

		return map;
	}

	private static final Object[] EMPTY_VALUES = new Object[0];

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public Object[] collectPositionalBindValues() {
		return EMPTY_VALUES;
//		if ( ! parameterMetadata.hasPositionalParameters() ) {
//			return EMPTY_VALUES;
//		}
//
//		final TreeMap sortedPositionalParamBindings = getSortedPositionalParamBindingMap();
//		final List values = CollectionHelper.arrayList( sortedPositionalParamBindings.size() );
//
//		for ( Map.Entry entry : sortedPositionalParamBindings.entrySet() ) {
//			if ( entry.getKey().getPosition() == null ) {
//				continue;
//			}
//			values.add( entry.getValue().getBindValue() );
//		}
//
//		return values.toArray( new Object[values.size()] );
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Map collectNamedParameterBindings() {
		final Map collectedBindings = new HashMap<>();

		for ( Map.Entry entry : parameterBindingMap.entrySet() ) {
			final String key;
			if ( entry.getKey().getPosition() != null ) {
				key = Integer.toString( entry.getKey().getPosition() );
			}
			else {
				key = entry.getKey().getName();
			}

			Type bindType = entry.getValue().getBindType();
			if ( bindType == null ) {
				log.debugf( "Binding for parameter [%s] did not define type", key );
				bindType = SerializableType.INSTANCE;
			}

			collectedBindings.put(
					key,
					new TypedValue( bindType, entry.getValue().getBindValue() )
			);
		}

		return collectedBindings;
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Parameter list binding - expect changes in 6.0

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public  QueryParameterListBinding getQueryParameterListBinding(QueryParameter queryParameter) {
		if ( parameterListBindingMap == null ) {
			parameterListBindingMap = new HashMap<>();
		}

		return transformQueryParameterBindingToQueryParameterListBinding( queryParameter );
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	private QueryParameterListBinding locateQueryParameterListBinding(QueryParameter queryParameter) {
		if ( parameterListBindingMap == null ) {
			parameterListBindingMap = new HashMap<>();
		}

		QueryParameterListBinding binding = parameterListBindingMap.get( queryParameter );
		if ( binding == null ) {
			QueryParameter resolved = resolveParameter( queryParameter );
			if ( resolved != queryParameter ) {
				binding = parameterListBindingMap.get( resolved );
			}
		}

		if ( binding == null ) {
			throw new IllegalArgumentException( "Could not locate parameter list binding" );
		}

		return binding;
	}

	private QueryParameter resolveParameter(QueryParameter queryParameter) {
		if ( queryParameter.getName() != null ) {
			return parameterMetadata.getQueryParameter( queryParameter.getName() );
		}
		else {
			return parameterMetadata.getQueryParameter( queryParameter.getPosition() );
		}
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameterListBinding transformQueryParameterBindingToQueryParameterListBinding(QueryParameter queryParameter) {
		log.debugf( "Converting QueryParameterBinding to QueryParameterListBinding for given QueryParameter : %s", queryParameter );

		getAndRemoveBinding( queryParameter );

		return makeListBinding( queryParameter );
	}

	private boolean shouldValidateBindingValue() {
		return sessionFactory.getSessionFactoryOptions().isJpaBootstrap() && queryParametersValidationEnabled;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameterBinding getAndRemoveBinding(QueryParameter parameter) {
		QueryParameterBinding binding = parameterBindingMap.remove( parameter );

		if ( binding == null ) {
			if ( parameter.getName() != null ) {
				parameter = parameterMetadata.getQueryParameter( parameter.getName() );
			}
			else {
				parameter = parameterMetadata.getQueryParameter( parameter.getPosition() );
			}

			if ( parameter == null ) {
				throw new HibernateException( "Unable to resolve QueryParameter" );
			}
		}
		binding = parameterBindingMap.remove( parameter );

		return binding;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public  QueryParameterListBinding getQueryParameterListBinding(String name) {
		// find the QueryParameter instance for the given name
		final QueryParameter queryParameter = resolveQueryParameter( name );
		return getQueryParameterListBinding( queryParameter );
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameter resolveQueryParameter(String name) {
		final QueryParameter param = parameterMetadata.getQueryParameter( name );

		if ( param == null ) {
			throw new IllegalArgumentException(
					"Unable to resolve given parameter name [" + name + "] to QueryParameter reference"
			);
		}

		return (QueryParameter) param;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public  QueryParameterListBinding getQueryParameterListBinding(int name) {
		// find the QueryParameter instance for the given name
		final QueryParameter queryParameter = resolveQueryParameter( name );
		return getQueryParameterListBinding( queryParameter );
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameter resolveQueryParameter(int name) {
		final QueryParameter param = parameterMetadata.getQueryParameter( name );

		if ( param == null ) {
			throw new IllegalArgumentException(
					"Unable to resolve given parameter name [" + name + "] to QueryParameter reference"
			);
		}

		return (QueryParameter) param;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public String expandListValuedParameters(String queryString, SharedSessionContractImplementor session) {
		if ( queryString == null ) {
			return null;
		}

		if ( parameterListBindingMap == null || parameterListBindingMap.isEmpty() ) {
			return queryString;
		}

		// more-or-less... for each entry in parameterListBindingMap we will create an
		//		entry in parameterBindingMap for each of the values in the bound value list.  afterwards
		//		we will clear the parameterListBindingMap.
		//
		// NOTE that this is essentially the legacy logical prior to modeling QueryParameterBinding/QueryParameterListBinding.
		// 		Fully expect the details of how this is handled in 6.0

		// HHH-1123
		// Some DBs limit number of IN expressions.  For now, warn...
		final Dialect dialect = session.getFactory().getServiceRegistry().getService( JdbcServices.class ).getJdbcEnvironment().getDialect();
		final int inExprLimit = dialect.getInExpressionCountLimit();

		int maxOrdinalPosition = getMaxOrdinalPosition();

		for ( Map.Entry entry : parameterListBindingMap.entrySet() ) {
			final QueryParameter sourceParam = entry.getKey();
			final Collection bindValues = entry.getValue().getBindValues();

			int bindValueCount = bindValues.size();
			int bindValueMaxCount = bindValueCount;

			boolean inClauseParameterPaddingEnabled =
					session.getFactory().getSessionFactoryOptions().inClauseParameterPaddingEnabled() &&
					bindValueCount > 2;

			if ( inClauseParameterPaddingEnabled ) {
				int bindValuePaddingCount = MathHelper.ceilingPowerOfTwo( bindValueCount );

				if ( bindValueCount < bindValuePaddingCount && (inExprLimit == 0 || bindValuePaddingCount < inExprLimit) ) {
					bindValueMaxCount = bindValuePaddingCount;
				}
			}

			if ( inExprLimit > 0 && bindValueCount > inExprLimit ) {
				log.tooManyInExpressions( dialect.getClass().getName(), inExprLimit, sourceParam.getName(), bindValueCount );
			}

			final String sourceToken;
			if ( sourceParam instanceof NamedParameterDescriptor ) {
				sourceToken = ":" + NamedParameterDescriptor.class.cast( sourceParam ).getName();
			}
			else {
				sourceToken = "?" + OrdinalParameterDescriptor.class.cast( sourceParam ).getPosition();
			}

			final int loc = StringHelper.indexOfIdentifierWord( queryString, sourceToken );

			if ( loc < 0 ) {
				continue;
			}

			final String beforePlaceholder = queryString.substring( 0, loc );
			final String afterPlaceholder = queryString.substring( loc + sourceToken.length() );

			// check if placeholder is already immediately enclosed in parentheses
			// (ignoring whitespace)
			boolean isEnclosedInParens =
					StringHelper.getLastNonWhitespaceCharacter( beforePlaceholder ) == '(' &&
							StringHelper.getFirstNonWhitespaceCharacter( afterPlaceholder ) == ')';

			if ( bindValues.size() == 1 && isEnclosedInParens ) {
				// short-circuit for performance when only 1 value and the
				// placeholder is already enclosed in parentheses...
				final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() );
				syntheticBinding.setBindValue( bindValues.iterator().next() );
				parameterBindingMap.put( sourceParam, syntheticBinding );
				continue;
			}

			StringBuilder expansionList = new StringBuilder();

			Iterator bindValueIterator = entry.getValue().getBindValues().iterator();
			Object bindValue = null;

			for ( int i = 0; i < bindValueMaxCount; i++ ) {

				if ( i < bindValueCount ) {
					bindValue = bindValueIterator.next();
				}

				if ( i > 0 ) {
					expansionList.append( ", " );
				}

				final QueryParameter syntheticParam;
				if ( sourceParam instanceof NamedParameterDescriptor ) {
					// in the case of a named parameter, for each value in the bound list-of-values we:
					//		1) create a synthetic named parameter
					//		2) expand the queryString to include each synthetic named param in place of the original
					//		3) create a new synthetic binding for just that single value under the synthetic name

					final String syntheticName = NamedParameterDescriptor.class.cast( sourceParam ).getName() + '_' + i;
					expansionList.append( ":" ).append( syntheticName );

					syntheticParam = new NamedParameterDescriptor(
							syntheticName,
							sourceParam.getHibernateType(),
							sourceParam.getSourceLocations()
					);
				}
				else {
					// in the case of an ordinal parameter, for each value in the bound list-of-values we:
					//		1) create a new ordinal parameter at a synthetic position of maxOrdinalPosition + 1
					//		2) expand the queryString to include each new ordinal param in place of the original
					//		3) create a new ordinal binding for just that single value under the synthetic position
					// for the first item, we reuse the original parameter to avoid gaps in the positions
					if ( i == 0 ) {
						syntheticParam = sourceParam;
					}
					else {
						int syntheticPosition = ++maxOrdinalPosition;
						syntheticParam = new OrdinalParameterDescriptor(
								syntheticPosition,
								syntheticPosition - jdbcStyleOrdinalCountBase,
								sourceParam.getHibernateType(),
								sourceParam.getSourceLocations()
						);
					}

					expansionList.append( "?" ).append( syntheticParam.getPosition() );
				}

				final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() );
				syntheticBinding.setBindValue( bindValue );
				parameterBindingMap.put( syntheticParam, syntheticBinding );
			}

			queryString = StringHelper.replace(
					beforePlaceholder,
					afterPlaceholder,
					sourceToken,
					expansionList.toString(),
					true,
					true
			);
		}

		return queryString;
	}

	private int getMaxOrdinalPosition() {
		int maxOrdinalPosition = 0;
		for ( QueryParameter queryParameter : parameterBindingMap.keySet() ) {
			if ( queryParameter instanceof OrdinalParameterDescriptor ) {
				maxOrdinalPosition = Math.max( maxOrdinalPosition, queryParameter.getPosition() );
			}
		}
		for ( QueryParameter queryParameter : parameterListBindingMap.keySet() ) {
			if ( queryParameter instanceof OrdinalParameterDescriptor ) {
				maxOrdinalPosition = Math.max( maxOrdinalPosition, queryParameter.getPosition() );
			}
		}
		return maxOrdinalPosition;
	}
}