org.springframework.data.jpa.repository.query.QueryUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.jpa.repository.query.QueryUtils.java

Source

/*
 * Copyright 2008-2019 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 static java.util.regex.Pattern.*;
import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Fetch;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
import javax.persistence.metamodel.Bindable;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.PluralAttribute;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort.JpaOrder;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Simple utility class to create JPA queries.
 *
 * @author Oliver Gierke
 * @author Kevin Raymond
 * @author Thomas Darimont
 * @author Komi Innocent
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Sbastien Pralta
 * @author Jens Schauder
 * @author Nils Borrmann
 * @author Reda.Housni-Alaoui
 * @author Florian Ldiger
 * @author Grgoire Druant
 * @author Mohammad Hewedy
 */
public abstract class QueryUtils {

    public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
    public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";

    // Used Regex/Unicode categories (see https://www.unicode.org/reports/tr18/#General_Category_Property):
    // Z Separator
    // Cc Control
    // Cf Format
    // P Punctuation
    private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
    static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
    static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);

    private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
    private static final String SIMPLE_COUNT_VALUE = "$2";
    private static final String COMPLEX_COUNT_VALUE = "$3 $6";
    private static final String COMPLEX_COUNT_LAST_VALUE = "$6";
    private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*";

    private static final Pattern ALIAS_MATCH;
    private static final Pattern COUNT_MATCH;
    private static final Pattern PROJECTION_CLAUSE = Pattern.compile("select\\s+(.+)\\s+from",
            Pattern.CASE_INSENSITIVE);

    private static final Pattern NO_DIGITS = Pattern.compile("\\D+");

    private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
    private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);

    private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
    private static final Pattern ORDER_BY = Pattern.compile(".*order\\s+by\\s+.*", CASE_INSENSITIVE);

    private static final Pattern NAMED_PARAMETER = Pattern
            .compile(COLON_NO_DOUBLE_COLON + IDENTIFIER + "|#" + IDENTIFIER, CASE_INSENSITIVE);

    private static final Pattern CONSTRUCTOR_EXPRESSION;

    private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;

    private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
    private static final int VARIABLE_NAME_GROUP_INDEX = 4;
    private static final int COMPLEX_COUNT_FIRST_INDEX = 3;

    private static final Pattern PUNCTATION_PATTERN = Pattern.compile(".*((?![._])[\\p{Punct}|\\s])");
    private static final Pattern FUNCTION_PATTERN;
    private static final Pattern FIELD_ALIAS_PATTERN;

    private static final String UNSAFE_PROPERTY_REFERENCE = "Sort expression '%s' must only contain property references or "
            + "aliases used in the select clause. If you really want to use something other than that for sorting, please use "
            + "JpaSort.unsafe()!";

    static {

        StringBuilder builder = new StringBuilder();
        builder.append("(?<=from)"); // from as starting delimiter
        builder.append("(?:\\s)+"); // at least one space separating
        builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
        builder.append("(?:\\sas)*"); // exclude possible "as" keyword
        builder.append("(?:\\s)+"); // at least one space separating
        builder.append("(?!(?:where|group\\s*by|order\\s*by))(\\w+)"); // the actual alias

        ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);

        builder = new StringBuilder();
        builder.append("(select\\s+((distinct)?((?s).+?)?)\\s+)?(from\\s+");
        builder.append(IDENTIFIER);
        builder.append("(?:\\s+as)?\\s+)");
        builder.append(IDENTIFIER_GROUP);
        builder.append("(.*)");

        COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);

        Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
        persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
        persistentAttributeTypes.put(ONE_TO_MANY, null);
        persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
        persistentAttributeTypes.put(MANY_TO_MANY, null);
        persistentAttributeTypes.put(ELEMENT_COLLECTION, null);

        ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);

        builder = new StringBuilder();
        builder.append("select");
        builder.append("\\s+"); // at least one space separating
        builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
        builder.append("new");
        builder.append("\\s+"); // at least one space separating
        builder.append(IDENTIFIER);
        builder.append("\\s*"); // zero to unlimited space separating
        builder.append("\\(");
        builder.append(".*");
        builder.append("\\)");

        CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);

        builder = new StringBuilder();
        // any function call including parameters within the brackets
        builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
        // the potential alias
        builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");

        FUNCTION_PATTERN = compile(builder.toString());

        builder = new StringBuilder();
        builder.append("\\s+"); // at least one space
        builder.append("[^\\s\\(\\)]+"); // No white char no bracket
        builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias

        FIELD_ALIAS_PATTERN = compile(builder.toString());

    }

    /**
     * Private constructor to prevent instantiation.
     */
    private QueryUtils() {

    }

    /**
     * Returns the query string to execute an exists query for the given id attributes.
     *
     * @param entityName the name of the entity to create the query for, must not be {@literal null}.
     * @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
     * @param idAttributes the id attributes for the entity, must not be {@literal null}.
     */
    public static String getExistsQueryString(String entityName, String countQueryPlaceHolder,
            Iterable<String> idAttributes) {

        String whereClause = Streamable.of(idAttributes).stream() //
                .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute, idAttribute)) //
                .collect(Collectors.joining(" AND ", " WHERE ", ""));

        return String.format(COUNT_QUERY_STRING, countQueryPlaceHolder, entityName) + whereClause;
    }

    /**
     * Returns the query string for the given class name.
     *
     * @param template must not be {@literal null}.
     * @param entityName must not be {@literal null}.
     * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
     */
    public static String getQueryString(String template, String entityName) {

        Assert.hasText(entityName, "Entity name must not be null or empty!");

        return String.format(template, entityName);
    }

    /**
     * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
     *
     * @param query the query string to which sorting is applied
     * @param sort the sort specification to apply.
     * @return the modified query string.
     */
    public static String applySorting(String query, Sort sort) {
        return applySorting(query, sort, detectAlias(query));
    }

    /**
     * Adds {@literal order by} clause to the JPQL query.
     *
     * @param query the query string to which sorting is applied. Must not be {@literal null} or empty.
     * @param sort the sort specification to apply.
     * @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
     * @return the modified query string.
     */
    public static String applySorting(String query, Sort sort, @Nullable String alias) {

        Assert.hasText(query, "Query must not be null or empty!");

        if (sort.isUnsorted()) {
            return query;
        }

        StringBuilder builder = new StringBuilder(query);

        if (!ORDER_BY.matcher(query).matches()) {
            builder.append(" order by ");
        } else {
            builder.append(", ");
        }

        Set<String> joinAliases = getOuterJoinAliases(query);
        Set<String> selectionAliases = getFunctionAliases(query);
        selectionAliases.addAll(getFieldAliases(query));

        for (Order order : sort) {
            builder.append(getOrderClause(joinAliases, selectionAliases, alias, order)).append(", ");
        }

        builder.delete(builder.length() - 2, builder.length());

        return builder.toString();
    }

    /**
     * Returns the order clause for the given {@link Order}. Will prefix the clause with the given alias if the referenced
     * property refers to a join alias, i.e. starts with {@code $alias.}.
     *
     * @param joinAliases the join aliases of the original query. Must not be {@literal null}.
     * @param alias the alias for the root entity. May be {@literal null}.
     * @param order the order object to build the clause for. Must not be {@literal null}.
     * @return a String containing a order clause. Guaranteed to be not {@literal null}.
     */
    private static String getOrderClause(Set<String> joinAliases, Set<String> selectionAlias,
            @Nullable String alias, Order order) {

        String property = order.getProperty();

        checkSortExpression(order);

        if (selectionAlias.contains(property)) {
            return String.format("%s %s", property, toJpaDirection(order));
        }

        boolean qualifyReference = !property.contains("("); // ( indicates a function

        for (String joinAlias : joinAliases) {
            if (property.startsWith(joinAlias.concat("."))) {
                qualifyReference = false;
                break;
            }
        }

        String reference = qualifyReference && StringUtils.hasText(alias) ? String.format("%s.%s", alias, property)
                : property;
        String wrapped = order.isIgnoreCase() ? String.format("lower(%s)", reference) : reference;

        return String.format("%s %s", wrapped, toJpaDirection(order));
    }

    /**
     * Returns the aliases used for {@code left (outer) join}s.
     *
     * @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
     * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
     */
    static Set<String> getOuterJoinAliases(String query) {

        Set<String> result = new HashSet<>();
        Matcher matcher = JOIN_PATTERN.matcher(query);

        while (matcher.find()) {

            String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
            if (StringUtils.hasText(alias)) {
                result.add(alias);
            }
        }

        return result;
    }

    /**
     * Returns the aliases used for fields in the query.
     *
     * @param query a {@literal String} containing a query. Must not be {@literal null}.
     * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
     */
    private static Set<String> getFieldAliases(String query) {
        Set<String> result = new HashSet<>();
        Matcher matcher = FIELD_ALIAS_PATTERN.matcher(query);

        while (matcher.find()) {
            String alias = matcher.group(1);

            if (StringUtils.hasText(alias)) {
                result.add(alias);
            }
        }
        return result;
    }

    /**
     * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
     *
     * @param query a {@literal String} containing a query. Must not be {@literal null}.
     * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
     */
    static Set<String> getFunctionAliases(String query) {

        Set<String> result = new HashSet<>();
        Matcher matcher = FUNCTION_PATTERN.matcher(query);

        while (matcher.find()) {

            String alias = matcher.group(1);

            if (StringUtils.hasText(alias)) {
                result.add(alias);
            }
        }

        return result;
    }

    private static String toJpaDirection(Order order) {
        return order.getDirection().name().toLowerCase(Locale.US);
    }

    /**
     * Resolves the alias for the entity to be retrieved from the given JPA query.
     *
     * @param query must not be {@literal null}.
     * @return Might return {@literal null}.
     * @deprecated use {@link DeclaredQuery#getAlias()} instead.
     */
    @Nullable
    @Deprecated
    public static String detectAlias(String query) {

        Matcher matcher = ALIAS_MATCH.matcher(query);

        return matcher.find() ? matcher.group(2) : null;
    }

    /**
     * Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
     * entities to the query.
     *
     * @param <T> type of the entities.
     * @param queryString must not be {@literal null}.
     * @param entities must not be {@literal null}.
     * @param entityManager must not be {@literal null}.
     * @return Guaranteed to be not {@literal null}.
     */

    public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {

        Assert.notNull(queryString, "Querystring must not be null!");
        Assert.notNull(entities, "Iterable of entities must not be null!");
        Assert.notNull(entityManager, "EntityManager must not be null!");

        Iterator<T> iterator = entities.iterator();

        if (!iterator.hasNext()) {
            return entityManager.createQuery(queryString);
        }

        String alias = detectAlias(queryString);
        StringBuilder builder = new StringBuilder(queryString);
        builder.append(" where");

        int i = 0;

        while (iterator.hasNext()) {

            iterator.next();

            builder.append(String.format(" %s = ?%d", alias, ++i));

            if (iterator.hasNext()) {
                builder.append(" or");
            }
        }

        Query query = entityManager.createQuery(builder.toString());

        iterator = entities.iterator();
        i = 0;

        while (iterator.hasNext()) {
            query.setParameter(++i, iterator.next());
        }

        return query;
    }

    /**
     * Creates a count projected query from the given original query.
     *
     * @param originalQuery must not be {@literal null} or empty.
     * @return Guaranteed to be not {@literal null}.
     * @deprecated use {@link DeclaredQuery#deriveCountQuery(String, String)} instead.
     */
    @Deprecated
    public static String createCountQueryFor(String originalQuery) {
        return createCountQueryFor(originalQuery, null);
    }

    /**
     * Creates a count projected query from the given original query.
     *
     * @param originalQuery must not be {@literal null}.
     * @param countProjection may be {@literal null}.
     * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
     * @since 1.6
     * @deprecated use {@link DeclaredQuery#deriveCountQuery(String, String)} instead.
     */
    @Deprecated
    public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) {

        Assert.hasText(originalQuery, "OriginalQuery must not be null or empty!");

        Matcher matcher = COUNT_MATCH.matcher(originalQuery);
        String countQuery;

        if (countProjection == null) {

            String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
            boolean useVariable = StringUtils.hasText(variable) //
                    && !variable.startsWith(" new") //
                    && !variable.startsWith("count(") //
                    && !variable.contains(","); //

            String complexCountValue = matcher.matches()
                    && StringUtils.hasText(matcher.group(COMPLEX_COUNT_FIRST_INDEX)) ? COMPLEX_COUNT_VALUE
                            : COMPLEX_COUNT_LAST_VALUE;

            String replacement = useVariable ? SIMPLE_COUNT_VALUE : complexCountValue;
            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
        } else {
            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, countProjection));
        }

        return countQuery.replaceFirst(ORDER_BY_PART, "");
    }

    /**
     * Returns whether the given {@link Query} contains named parameters.
     *
     * @param query Must not be {@literal null}.
     * @return whether the given {@link Query} contains named parameters.
     */
    public static boolean hasNamedParameter(Query query) {

        Assert.notNull(query, "Query must not be null!");

        for (Parameter<?> parameter : query.getParameters()) {

            String name = parameter.getName();

            // Hibernate 3 specific hack as it returns the index as String for the name.
            if (name != null && NO_DIGITS.matcher(name).find()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns whether the given query contains named parameters.
     *
     * @param query can be {@literal null} or empty.
     * @return whether the given query contains named parameters.
     */
    @Deprecated
    static boolean hasNamedParameter(@Nullable String query) {
        return StringUtils.hasText(query) && NAMED_PARAMETER.matcher(query).find();
    }

    /**
     * Turns the given {@link Sort} into {@link javax.persistence.criteria.Order}s.
     *
     * @param sort the {@link Sort} instance to be transformed into JPA {@link javax.persistence.criteria.Order}s.
     * @param from must not be {@literal null}.
     * @param cb must not be {@literal null}.
     * @return a {@link List} of {@link javax.persistence.criteria.Order}s.
     */
    public static List<javax.persistence.criteria.Order> toOrders(Sort sort, From<?, ?> from, CriteriaBuilder cb) {

        if (sort.isUnsorted()) {
            return Collections.emptyList();
        }

        Assert.notNull(from, "From must not be null!");
        Assert.notNull(cb, "CriteriaBuilder must not be null!");

        List<javax.persistence.criteria.Order> orders = new ArrayList<>();

        for (org.springframework.data.domain.Sort.Order order : sort) {
            orders.add(toJpaOrder(order, from, cb));
        }

        return orders;
    }

    /**
     * Returns whether the given JPQL query contains a constructor expression.
     *
     * @param query must not be {@literal null} or empty.
     * @return whether the given JPQL query contains a constructor expression.
     * @since 1.10
     */
    public static boolean hasConstructorExpression(String query) {

        Assert.hasText(query, "Query must not be null or empty!");

        return CONSTRUCTOR_EXPRESSION.matcher(query).find();
    }

    /**
     * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
     *
     * @param query must not be {@literal null} or empty.
     * @return the projection part of the query.
     * @since 1.10.2
     */
    public static String getProjection(String query) {

        Assert.hasText(query, "Query must not be null or empty!");

        Matcher matcher = PROJECTION_CLAUSE.matcher(query);
        String projection = matcher.find() ? matcher.group(1) : "";
        return projection.trim();
    }

    /**
     * Creates a criteria API {@link javax.persistence.criteria.Order} from the given {@link Order}.
     *
     * @param order the order to transform into a JPA {@link javax.persistence.criteria.Order}
     * @param from the {@link From} the {@link Order} expression is based on
     * @param cb the {@link CriteriaBuilder} to build the {@link javax.persistence.criteria.Order} with
     * @return Guaranteed to be not {@literal null}.
     */
    @SuppressWarnings("unchecked")
    private static javax.persistence.criteria.Order toJpaOrder(Order order, From<?, ?> from, CriteriaBuilder cb) {

        PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType());
        Expression<?> expression = toExpressionRecursively(from, property);

        if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) {
            Expression<String> lower = cb.lower((Expression<String>) expression);
            return order.isAscending() ? cb.asc(lower) : cb.desc(lower);
        } else {
            return order.isAscending() ? cb.asc(expression) : cb.desc(expression);
        }
    }

    static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
        return toExpressionRecursively(from, property, false);
    }

    @SuppressWarnings("unchecked")
    static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property,
            boolean isForSelection) {

        Bindable<?> propertyPathModel;
        Bindable<?> model = from.getModel();
        String segment = property.getSegment();

        if (model instanceof ManagedType) {

            /*
             *  Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
             *  See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
             */
            propertyPathModel = (Bindable<?>) ((ManagedType<?>) model).getAttribute(segment);
        } else {
            propertyPathModel = from.get(segment).getModel();
        }

        if (requiresOuterJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(),
                isForSelection) && !isAlreadyFetched(from, segment)) {
            Join<?, ?> join = getOrCreateJoin(from, segment);
            return (Expression<T>) (property.hasNext()
                    ? toExpressionRecursively(join, property.next(), isForSelection)
                    : join);
        } else {
            Path<Object> path = from.get(segment);
            return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
        }
    }

    /**
     * Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
     * optional association.
     *
     * @param propertyPathModel may be {@literal null}.
     * @param isPluralAttribute is the attribute of Collection type?
     * @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
     * @param isForSelection is the property navigated for the selection part of the query?
     * @return whether an outer join is to be used for integrating this attribute in a query.
     */
    private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
            boolean isLeafProperty, boolean isForSelection) {

        if (propertyPathModel == null && isPluralAttribute) {
            return true;
        }

        if (!(propertyPathModel instanceof Attribute)) {
            return false;
        }

        Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;

        if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
            return false;
        }

        // if this path is an optional one to one attribute navigated from the not owning side we also need an explicit
        // outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 and
        // https://github.com/eclipse-ee4j/jpa-api/issues/170
        boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute
                .getPersistentAttributeType()
                && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));

        // if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
        // to use an inner join instead.
        // see https://hibernate.atlassian.net/browse/HHH-12999.
        if (isLeafProperty && !isForSelection && !attribute.isCollection() && !isInverseOptionalOneToOne) {
            return false;
        }

        return getAnnotationProperty(attribute, "optional", true);
    }

    private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {

        Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES
                .get(attribute.getPersistentAttributeType());

        if (associationAnnotation == null) {
            return defaultValue;
        }

        Member member = attribute.getJavaMember();

        if (!(member instanceof AnnotatedElement)) {
            return defaultValue;
        }

        Annotation annotation = AnnotationUtils.getAnnotation((AnnotatedElement) member, associationAnnotation);
        return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
    }

    static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPath property) {

        Path<Object> result = path.get(property.getSegment());
        return property.hasNext() ? toExpressionRecursively(result, property.next()) : result;
    }

    /**
     * Returns an existing join for the given attribute if one already exists or creates a new one if not.
     *
     * @param from the {@link From} to get the current joins from.
     * @param attribute the {@link Attribute} to look for in the current joins.
     * @return will never be {@literal null}.
     */
    private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {

        for (Join<?, ?> join : from.getJoins()) {

            boolean sameName = join.getAttribute().getName().equals(attribute);

            if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
                return join;
            }
        }

        return from.join(attribute, JoinType.LEFT);
    }

    /**
     * Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
     *
     * @param from the {@link From} to check for fetches.
     * @param attribute the attribute name to check.
     * @return
     */
    private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {

        for (Fetch<?, ?> fetch : from.getFetches()) {

            boolean sameName = fetch.getAttribute().getName().equals(attribute);

            if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check any given {@link JpaOrder#isUnsafe()} order for presence of at least one property offending the
     * {@link #PUNCTATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression.
     *
     * @param order
     */
    private static void checkSortExpression(Order order) {

        if (order instanceof JpaOrder && ((JpaOrder) order).isUnsafe()) {
            return;
        }

        if (PUNCTATION_PATTERN.matcher(order.getProperty()).find()) {
            throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order));
        }
    }
}