JpqlUtils.java
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.util.Assert;
/**
* Utilities to create JPQL expressions, derived from {@link QueryUtils}.
*
* @author Mark Paluch
*/
class JpqlUtils {
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property) {
return toExpressionRecursively(metamodel, source, from, property, false);
}
static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property, boolean isForSelection) {
return JpqlExpressionFactory.INSTANCE.toExpressionRecursively(metamodel, source, from, property, isForSelection,
false);
}
/**
* Expression Factory for JPQL queries that operate on String-based queries.
*/
static class JpqlExpressionFactory extends ExpressionFactorySupport {
private static final JpqlExpressionFactory INSTANCE = new JpqlExpressionFactory();
/**
* Creates an expression with proper inner and left joins by recursively navigating the path
*
* @param metamodel the JPA {@link Metamodel} used to resolve attribute types to {@link ManagedType}.
* @param source the {@link org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin}
* @param from bindable from which the property is navigated.
* @param property the property path
* @param isForSelection is the property navigated for the selection or ordering part of the query?
* @param hasRequiredOuterJoin has a parent already required an outer join?
* @return the expression
*/
public JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source,
Bindable<?> from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) {
String segment = property.getSegment();
boolean isLeafProperty = !property.hasNext();
BindablePathResolver resolver = new BindablePathResolver(metamodel, from);
boolean isRelationshipId = isRelationshipId(resolver, property);
boolean requiresOuterJoin = requiresOuterJoin(resolver, property, isForSelection, hasRequiredOuterJoin,
isLeafProperty, isRelationshipId);
// if it does not require an outer join and is a leaf, simply get the segment
if (!requiresOuterJoin && (isLeafProperty || isRelationshipId)) {
return new JpqlQueryBuilder.PathAndOrigin(property, source, false);
}
// get or create the join
JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment)
: JpqlQueryBuilder.innerJoin(source, segment);
// if it's a leaf, return the join
if (isLeafProperty) {
return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true);
}
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
ManagedType<?> managedTypeForModel = getManagedTypeForModel(from);
Attribute<?, ?> nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from);
if (nextAttribute == null) {
throw new IllegalStateException("Binding property is null");
}
return toExpressionRecursively(metamodel, joinSource, (Bindable<?>) nextAttribute, nextProperty, isForSelection,
requiresOuterJoin);
}
private static @Nullable Attribute<?, ?> getModelForPath(@Nullable Metamodel metamodel, PropertyPath path,
@Nullable ManagedType<?> managedType, @Nullable Bindable<?> fallback) {
String segment = path.getSegment();
if (managedType != null) {
try {
return managedType.getAttribute(segment);
} catch (IllegalArgumentException ex) {
// ManagedType may be erased for some vendor if the attribute is declared as generic
}
}
if (metamodel != null && fallback != null) {
Class<?> fallbackType = fallback.getBindableJavaType();
try {
return metamodel.managedType(fallbackType).getAttribute(segment);
} catch (IllegalArgumentException e) {
// nothing to do here
}
}
return null;
}
record BindablePathResolver(Metamodel metamodel,
Bindable<?> bindable) implements ExpressionFactorySupport.ModelPathResolver {
@Override
public @Nullable Bindable<?> resolve(PropertyPath propertyPath) {
Attribute<?, ?> attribute = resolveAttribute(propertyPath);
return attribute instanceof Bindable<?> b ? b : null;
}
private @Nullable Attribute<?, ?> resolveAttribute(PropertyPath propertyPath) {
ManagedType<?> managedType = getManagedTypeForModel(bindable);
return getModelForPath(metamodel, propertyPath, managedType, bindable);
}
@Override
@SuppressWarnings("NullAway")
public @Nullable Bindable<?> resolveNext(PropertyPath propertyPath) {
Assert.state(propertyPath.hasNext(), "PropertyPath must contain at least one element");
Attribute<?, ?> propertyPathModel = resolveAttribute(propertyPath);
ManagedType<?> propertyPathManagedType = getManagedTypeForModel(propertyPathModel);
Attribute<?, ?> next = getModelForPath(metamodel, Objects.requireNonNull(propertyPath.next()),
propertyPathManagedType, null);
return next instanceof Bindable<?> b ? b : null;
}
}
}
}