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

io.micronaut.data.runtime.query.internal.DefaultStoredQuery Maven / Gradle / Ivy

There is a newer version: 4.10.5
Show newest version
/*
 * Copyright 2017-2022 original 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 io.micronaut.data.runtime.query.internal;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.*;
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.intercept.annotation.DataMethodQuery;
import io.micronaut.data.intercept.annotation.DataMethodQueryParameter;
import io.micronaut.data.model.AssociationUtils;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.JsonDataType;
import io.micronaut.data.model.query.JoinPath;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import io.micronaut.data.model.runtime.DefaultStoredDataOperation;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.operations.HintsCapableRepository;
import io.micronaut.inject.ExecutableMethod;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static io.micronaut.data.intercept.annotation.DataMethod.META_MEMBER_LIMIT;
import static io.micronaut.data.intercept.annotation.DataMethod.META_MEMBER_PAGE_SIZE;

/**
 * Represents a prepared query.
 *
 * @param   The entity type
 * @param  The result type
 */
@Internal
public final class DefaultStoredQuery extends DefaultStoredDataOperation implements StoredQuery {
    private static final String DATA_METHOD_ANN_NAME = DataMethod.class.getName();
    @NonNull
    private final Class resultType;
    private final DataType resultDataType;
    @NonNull
    private final Class rootEntity;
    @NonNull
    private final String query;
    private final String[] queryParts;
    private final ExecutableMethod method;
    private final boolean isDto;
    private final boolean isOptimisticLock;
    private final boolean isNative;
    private final boolean isProcedure;
    private final boolean isNumericPlaceHolder;
    private final boolean hasPageable;
    private final AnnotationMetadata annotationMetadata;
    private final boolean isCount;
    private final boolean hasResultConsumer;
    private Map queryHints;
    private Set joinPaths = null;
    private Set joinFetchPaths = null;
    private final List queryParameters;
    private final boolean rawQuery;
    private final boolean jsonEntity;
    private final OperationType operationType;
    private final Map> parameterExpressions;
    private final int limit;
    private final int offset;

    /**
     * The default constructor.
     *
     * @param method               The target method
     * @param isCount              Is the query a count query
     * @param repositoryOperations The repositoryOperations
     */
    public DefaultStoredQuery(
        @NonNull ExecutableMethod method,
        boolean isCount,
        HintsCapableRepository repositoryOperations) {
        this(method, method.getAnnotation(DataMethod.NAME), isCount, repositoryOperations);
    }

    /**
     * The default constructor.
     *
     * @param method               The target method
     * @param dataMethodQuery      The data method query annotation
     * @param repositoryOperations The repositoryOperations
     */
    public DefaultStoredQuery(
        @NonNull ExecutableMethod method,
        AnnotationValue dataMethodQuery,
        HintsCapableRepository repositoryOperations) {
        this(method, dataMethodQuery, false, repositoryOperations);
    }

    /**
     * The default constructor.
     *
     * @param method               The target method
     * @param dataMethodQuery      The data method query annotation
     * @param isCount              Is the query a count query
     * @param repositoryOperations The repositoryOperations
     */
    public DefaultStoredQuery(
        @NonNull ExecutableMethod method,
        AnnotationValue dataMethodQuery,
        boolean isCount,
        HintsCapableRepository repositoryOperations) {
        super(method);

        this.rootEntity = getRequiredRootEntity(method);
        this.annotationMetadata = method.getAnnotationMetadata();
        this.isProcedure = dataMethodQuery.isTrue(DataMethodQuery.META_MEMBER_PROCEDURE);
        this.hasResultConsumer = method.stringValue(DATA_METHOD_ANN_NAME, "sqlMappingFunction").isPresent();
        this.isNumericPlaceHolder = method
                .classValue(RepositoryConfiguration.class, "queryBuilder")
                .map(c -> c == SqlQueryBuilder.class).orElse(false);
        this.hasPageable = dataMethodQuery.stringValue(TypeRole.PAGEABLE).isPresent() ||
            dataMethodQuery.stringValue(TypeRole.SORT).isPresent() ||
            dataMethodQuery.intValue(META_MEMBER_LIMIT).orElse(-1) > -1 ||
            dataMethodQuery.intValue(META_MEMBER_PAGE_SIZE).orElse(-1) > -1;
        String query;
        if (isCount) {
            // Legacy count definition
            AnnotationValue queryAnnotation = method.getAnnotation(Query.class.getName());
            query = queryAnnotation.stringValue(DataMethod.META_MEMBER_COUNT_QUERY)
                .orElseGet(() -> queryAnnotation.stringValue()
                    .orElseThrow(() -> new IllegalStateException("No query present in method")));
            Optional rawCountQueryString = method.stringValue(Query.class, DataMethod.META_MEMBER_RAW_COUNT_QUERY);
            this.rawQuery = rawCountQueryString.isPresent();
            this.query = rawCountQueryString.orElse(query);
            String[] countQueryParts = method.stringValues(DataMethod.class, DataMethod.META_MEMBER_EXPANDABLE_COUNT_QUERY);
            // for countBy queries this is empty, and we should use DataMethod.META_MEMBER_EXPANDABLE_QUERY value
            if (ArrayUtils.isNotEmpty(countQueryParts)) {
                this.queryParts = countQueryParts;
            } else {
                this.queryParts = method.stringValues(DataMethodQuery.class, DataMethodQuery.META_MEMBER_EXPANDABLE_QUERY);
            }
            this.isNative = queryAnnotation.isTrue(DataMethodQuery.META_MEMBER_NATIVE);
            //noinspection unchecked
            this.resultType = (Class) Long.class;
            this.resultDataType = DataType.LONG;
        } else {
            Optional q = dataMethodQuery.stringValue();
            if (q.isPresent()) {
                // Query defined by DataMethodQuery
                Optional rawQueryString = dataMethodQuery.stringValue(DataMethodQuery.META_MEMBER_RAW_QUERY);
                this.isNative = dataMethodQuery.isTrue(DataMethodQuery.META_MEMBER_NATIVE);
                this.rawQuery = rawQueryString.isPresent();
                this.query = rawQueryString.orElseGet(q::get);
            } else {
                AnnotationValue queryAnnotation = method.getAnnotation(Query.class.getName());
                query = queryAnnotation.stringValue().orElseThrow(() ->
                    new IllegalStateException("No query present in method")
                );
                Optional rawQueryString = queryAnnotation.stringValue(DataMethodQuery.META_MEMBER_RAW_QUERY);
                this.isNative = queryAnnotation.isTrue(DataMethodQuery.META_MEMBER_NATIVE);
                this.rawQuery = rawQueryString.isPresent();
                this.query = rawQueryString.orElse(query);
            }
            this.resultDataType = dataMethodQuery.enumValue(DataMethodQuery.META_MEMBER_RESULT_DATA_TYPE, DataType.class).orElse(DataType.OBJECT);
            this.queryParts = dataMethodQuery.stringValues(DataMethodQuery.META_MEMBER_EXPANDABLE_QUERY);
            //noinspection unchecked
            this.resultType = dataMethodQuery.classValue(DataMethodQuery.META_MEMBER_RESULT_TYPE)
                .map(type -> (Class) ReflectionUtils.getWrapperType(type))
                .orElse((Class) rootEntity);
        }
        this.method = method;
        this.isDto = dataMethodQuery.isTrue(DataMethodQuery.META_MEMBER_DTO);
        this.isOptimisticLock = dataMethodQuery.isTrue(DataMethodQuery.META_MEMBER_OPTIMISTIC_LOCK);
        this.operationType = dataMethodQuery.enumValue(DataMethodQuery.META_MEMBER_OPERATION_TYPE, DataMethodQuery.OperationType.class)
            .map(op -> OperationType.valueOf(op.name()))
            .orElse(OperationType.QUERY);
        this.isCount = isCount || operationType == OperationType.COUNT;
        if (method.hasAnnotation(QueryHint.class)) {
            List> values = method.getAnnotationValuesByType(QueryHint.class);
            this.queryHints = new HashMap<>(values.size());
            for (AnnotationValue value : values) {
                String n = value.stringValue("name").orElse(null);
                String v = value.stringValue("value").orElse(null);
                if (StringUtils.isNotEmpty(n) && StringUtils.isNotEmpty(v)) {
                    queryHints.put(n, v);
                }
            }
        }
        Map queryHints = repositoryOperations.getQueryHints(this);
        if (queryHints != Collections.EMPTY_MAP) {
            if (this.queryHints != null) {
                this.queryHints.putAll(queryHints);
            } else {
                this.queryHints = queryHints;
            }
        }

        this.queryParameters = getQueryParameters(
            dataMethodQuery.getAnnotations(DataMethodQuery.META_MEMBER_PARAMETERS, DataMethodQueryParameter.class),
            isNumericPlaceHolder
        );
        this.jsonEntity = DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(annotationMetadata);
        this.parameterExpressions = annotationMetadata.getAnnotationValuesByType(ParameterExpression.class).stream()
            .collect(Collectors.toMap(av -> av.stringValue("name").orElseThrow(), av -> av));
        this.limit = dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_LIMIT).orElse(-1);
        this.offset = dataMethodQuery.intValue(DataMethodQuery.META_MEMBER_OFFSET).orElse(0);
    }

    private static  Class getRequiredRootEntity(ExecutableMethod context) {
        Class aClass = context.classValue(DataMethod.NAME, DataMethod.META_MEMBER_ROOT_ENTITY).orElse(null);
        if (aClass != null) {
            return aClass;
        } else {
            final AnnotationValue ann = context.getDeclaredAnnotation(DataMethod.NAME);
            if (ann != null) {
                aClass = ann.classValue(DataMethod.META_MEMBER_ROOT_ENTITY).orElse(null);
                if (aClass != null) {
                    return aClass;
                }
            }
            throw new IllegalStateException("No root entity present in method");
        }
    }

    private static List getQueryParameters(List> params,
                                                                  boolean isNumericPlaceHolder) {
        List queryParameters = new ArrayList<>(params.size());
        for (AnnotationValue av : params) {
            String[] propertyPath = av.stringValues(DataMethodQueryParameter.META_MEMBER_PROPERTY_PATH);
            Object value = null;
            if (av.getValues().containsKey(AnnotationMetadata.VALUE_MEMBER)) {
                value = av;
            }
            if (propertyPath.length == 0) {
                propertyPath = av.stringValue(DataMethodQueryParameter.META_MEMBER_PROPERTY)
                        .map(property -> new String[]{property})
                        .orElse(null);
            }
            String[] parameterBindingPath = av.stringValues(DataMethodQueryParameter.META_MEMBER_PARAMETER_BINDING_PATH);
            if (parameterBindingPath.length == 0) {
                parameterBindingPath = null;
            }
            DataType dataType = isNumericPlaceHolder ? av.enumValue(DataMethodQueryParameter.META_MEMBER_DATA_TYPE, DataType.class).orElse(DataType.OBJECT) : null;
            JsonDataType jsonDataType = dataType != null ? av.enumValue(DataMethodQueryParameter.META_MEMBER_JSON_DATA_TYPE, JsonDataType.class).orElse(JsonDataType.DEFAULT) : null;
            queryParameters.add(
                    new StoredQueryParameter(
                            av.stringValue(DataMethodQueryParameter.META_MEMBER_NAME).orElse(null),
                            dataType,
                            jsonDataType,
                            av.intValue(DataMethodQueryParameter.META_MEMBER_PARAMETER_INDEX).orElse(-1),
                            parameterBindingPath,
                            propertyPath,
                            av.booleanValue(DataMethodQueryParameter.META_MEMBER_AUTO_POPULATED).orElse(false),
                            av.booleanValue(DataMethodQueryParameter.META_MEMBER_REQUIRES_PREVIOUS_POPULATED_VALUES).orElse(false),
                            av.classValue(DataMethodQueryParameter.META_MEMBER_CONVERTER).orElse(null),
                            av.booleanValue(DataMethodQueryParameter.META_MEMBER_EXPANDABLE).orElse(false),
                            av.booleanValue(DataMethodQueryParameter.META_MEMBER_EXPRESSION).orElse(false),
                            value,
                            av.stringValue(DataMethodQueryParameter.META_MEMBER_ROLE).orElse(null),
                            av.stringValue(DataMethodQueryParameter.META_MEMBER_TABLE_ALIAS).orElse(null),
                            queryParameters
                    ));
        }
        return queryParameters;
    }

    @Override
    public int getLimit() {
        return limit;
    }

    @Override
    public int getOffset() {
        return offset;
    }

    @Override
    public List getQueryBindings() {
        return queryParameters;
    }

    @NonNull
    @Override
    public Set getJoinFetchPaths() {
        if (joinFetchPaths == null) {
            this.joinFetchPaths = Collections.unmodifiableSet(AssociationUtils.getJoinFetchPaths(method));
        }
        return joinFetchPaths;
    }

    @Override
    public Set getJoinPaths() {
        if (joinPaths == null) {
            joinPaths = Collections.unmodifiableSet(AssociationUtils.getJoinPaths(method));
        }
        return joinPaths;
    }

    /**
     * @return The method
     */
    public ExecutableMethod getMethod() {
        return method;
    }

    @Override
    public boolean isSingleResult() {
        return !isCount() && getJoinFetchPaths().isEmpty();
    }

    @Override
    public boolean hasResultConsumer() {
        return this.hasResultConsumer;
    }

    @Override
    public boolean isCount() {
        return isCount;
    }

    @NonNull
    @Override
    public Map getQueryHints() {
        if (queryHints != null) {
            return queryHints;
        }
        return Collections.emptyMap();
    }

    @Override
    public boolean isNative() {
        return isNative;
    }

    @Override
    public boolean isProcedure() {
        return isProcedure;
    }

    @Override
    public OperationType getOperationType() {
        return operationType;
    }

    /**
     * Is this a raw SQL query.
     *
     * @return The raw sql query.
     */
    @Override
    public boolean useNumericPlaceholders() {
        return isNumericPlaceHolder;
    }

    /**
     * @return Whether the query is a DTO query
     */
    @Override
    public boolean isDtoProjection() {
        return isDto;
    }

    /**
     * @return The result type
     */
    @Override
    @NonNull
    public Class getResultType() {
        return resultType;
    }

    @NonNull
    @Override
    public DataType getResultDataType() {
        return resultDataType;
    }

    /**
     * @return The ID type
     */
    @SuppressWarnings("unchecked")
    @Override
    public Optional> getEntityIdentifierType() {
        Optional o = annotationMetadata.classValue(DATA_METHOD_ANN_NAME, DataMethod.META_MEMBER_ID_TYPE);
        return o;
    }

    /**
     * @return The root entity type
     */
    @Override
    @NonNull
    public Class getRootEntity() {
        return rootEntity;
    }

    @Override
    public boolean hasPageable() {
        return hasPageable;
    }

    @Override
    @NonNull
    public String getQuery() {
        return query;
    }

    @Override
    public String[] getExpandableQueryParts() {
        return queryParts;
    }

    @NonNull
    @Override
    public String getName() {
        return method.getMethodName();
    }

    @Override
    @NonNull
    public Class[] getArgumentTypes() {
        return method.getArgumentTypes();
    }

    @Override
    public boolean isOptimisticLock() {
        return isOptimisticLock;
    }

    @Override
    public boolean isRawQuery() {
        return this.rawQuery;
    }

    @Override
    public boolean isJsonEntity() {
        return jsonEntity;
    }

    @Override
    public Map> getParameterExpressions() {
        return parameterExpressions;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DefaultStoredQuery that = (DefaultStoredQuery) o;
        return resultType.equals(that.resultType) &&
                method.equals(that.method);
    }

    @Override
    public int hashCode() {
        return Objects.hash(resultType, method);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy