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

io.micronaut.data.hibernate.operations.HibernateJpaOperations Maven / Gradle / Ivy

There is a newer version: 4.10.5
Show newest version
/*
 * Copyright 2017-2020 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.hibernate.operations;

import io.micronaut.aop.InvocationContext;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.data.annotation.QueryHint;
import io.micronaut.data.annotation.sql.Procedure;
import io.micronaut.data.hibernate.conf.RequiresSyncHibernate;
import io.micronaut.data.jpa.annotation.EntityGraph;
import io.micronaut.data.jpa.operations.JpaRepositoryOperations;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.runtime.BatchOperation;
import io.micronaut.data.model.runtime.DeleteBatchOperation;
import io.micronaut.data.model.runtime.DeleteOperation;
import io.micronaut.data.model.runtime.EntityInstanceOperation;
import io.micronaut.data.model.runtime.InsertBatchOperation;
import io.micronaut.data.model.runtime.InsertOperation;
import io.micronaut.data.model.runtime.PagedQuery;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.model.runtime.UpdateBatchOperation;
import io.micronaut.data.model.runtime.UpdateOperation;
import io.micronaut.data.operations.CriteriaRepositoryOperations;
import io.micronaut.data.operations.async.AsyncCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveRepositoryOperations;
import io.micronaut.data.runtime.convert.DataConversionService;
import io.micronaut.data.runtime.operations.ExecutorAsyncOperations;
import io.micronaut.data.runtime.operations.ExecutorAsyncOperationsSupportingCriteria;
import io.micronaut.data.runtime.operations.ExecutorReactiveOperationsSupportingCriteria;
import io.micronaut.transaction.TransactionOperations;
import jakarta.inject.Named;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.CriteriaUpdate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.graph.RootGraph;
import org.hibernate.procedure.ProcedureCall;
import org.hibernate.query.CommonQueryContract;
import org.hibernate.query.MutationQuery;
import org.hibernate.query.Query;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.stream.Stream;

/**
 * Implementation of the {@link JpaRepositoryOperations} interface for Hibernate.
 *
 * @author graemerocher
 * @since 1.0
 */
@RequiresSyncHibernate
@EachBean(DataSource.class)
final class HibernateJpaOperations extends AbstractHibernateOperations>
    implements JpaRepositoryOperations, AsyncCapableRepository, ReactiveCapableRepository, CriteriaRepositoryOperations {

    private final SessionFactory sessionFactory;
    private final TransactionOperations transactionOperations;
    private ExecutorAsyncOperations asyncOperations;
    private ExecutorService executorService;

    /**
     * Default constructor.
     *
     * @param sessionFactory        The session factory
     * @param transactionOperations The transaction operations
     * @param executorService       The executor service for I/O tasks to use
     * @param runtimeEntityRegistry The runtime entity registry
     * @param dataConversionService The data conversion service
     */
    public HibernateJpaOperations(
        @NonNull @Parameter SessionFactory sessionFactory,
        @NonNull @Parameter TransactionOperations transactionOperations,
        @Named("io") @Nullable ExecutorService executorService,
        RuntimeEntityRegistry runtimeEntityRegistry,
        DataConversionService dataConversionService) {
        super(runtimeEntityRegistry, dataConversionService);
        ArgumentUtils.requireNonNull("sessionFactory", sessionFactory);
        this.sessionFactory = sessionFactory;
        this.transactionOperations = transactionOperations;
        this.executorService = executorService;
    }

    @Override
    public  RuntimePersistentEntity getEntity(Class type) {
        return runtimeEntityRegistry.getEntity(type);
    }

    @Override
    public ApplicationContext getApplicationContext() {
        return super.getApplicationContext();
    }

    @Override
    public ConversionService getConversionService() {
        return super.getConversionService();
    }

    @Override
    protected void setParameter(CommonQueryContract query, String parameterName, Object value) {
        query.setParameter(parameterName, value);
    }

    @Override
    protected void setParameter(CommonQueryContract query, String parameterName, Object value, Argument argument) {
        // How to provide type, if needed at all? Was needed prior to Hibernate 6
        query.setParameter(parameterName, value);
    }

    @Override
    protected void setParameterList(CommonQueryContract query, String parameterName, Collection value) {
        if (value == null) {
            value = Collections.emptyList();
        }
        // Passing collection as param like this as well, before Hibernate 6 there was other method to pass collection
        query.setParameterList(parameterName, value);
    }

    @Override
    protected void setParameterList(CommonQueryContract query, String parameterName, Collection value, Argument argument) {
        if (value == null) {
            value = Collections.emptyList();
        }
        // Can we ignore type? Was needed before Hibernate 6
        query.setParameterList(parameterName, value);
    }

    @Override
    protected void setParameter(CommonQueryContract query, int parameterIndex, Object value) {
        query.setParameter(parameterIndex, value);
    }

    @Override
    protected void setParameter(CommonQueryContract query, int parameterIndex, Object value, Argument argument) {
        query.setParameter(parameterIndex, value);
    }

    @Override
    protected void setParameterList(CommonQueryContract query, int parameterIndex, Collection value) {
        if (value == null) {
            value = Collections.emptyList();
        }
        query.setParameterList(parameterIndex, value);
    }

    @Override
    protected void setParameterList(CommonQueryContract query, int parameterIndex, Collection value, Argument argument) {
        if (value == null) {
            value = Collections.emptyList();
        }
        // Can we ignore type? Was needed before Hibernate 6
        query.setParameterList(parameterIndex, value);
    }

    @Override
    protected void setHint(Query query, String hintName, Object value) {
        query.setHint(hintName, value);
    }

    @Override
    protected  RootGraph getEntityGraph(Session session, Class entityType, String graphName) {
        return (RootGraph) session.getEntityGraph(graphName);
    }

    @Override
    protected  RootGraph createEntityGraph(Session session, Class entityType) {
        return session.createEntityGraph(entityType);
    }

    @Override
    protected Query createQuery(Session session, String query, Class resultType) {
        return session.createQuery(query, resultType);
    }

    @Override
    protected Query createNativeQuery(Session session, String query, Class resultType) {
        return session.createNativeQuery(query, resultType);
    }

    @Override
    protected Query createQuery(Session session, CriteriaQuery criteriaQuery) {
        return session.createQuery(criteriaQuery);
    }

    @Override
    protected void setOffset(Query query, int offset) {
        query.setFirstResult(offset);
    }

    @Override
    protected void setMaxResults(Query query, int max) {
        query.setMaxResults(max);
    }

    @NonNull
    @Override
    public Map getQueryHints(@NonNull StoredQuery storedQuery) {
        return super.getQueryHints(storedQuery);
    }

    @Nullable
    @Override
    public  T findOne(@NonNull Class type, @NonNull Object id) {
        return executeRead(session -> session.byId(type).load(id));
    }

    @NonNull
    @Override
    public  T load(@NonNull Class type, @NonNull Object id) {
        return executeRead(session -> session.getReference(type, id));
    }

    @Override
    public  T merge(T entity) {
        return executeWrite(session -> session.merge(entity));
    }

    @Nullable
    @Override
    public  R findOne(@NonNull PreparedQuery preparedQuery) {
        return executeRead(session -> {
            // limit does not work with native queries and does not produce expected
            // results with EntityGraph annotation and joins
            boolean limitOne = !preparedQuery.isNative() && !hasEntityGraph(preparedQuery.getAnnotationMetadata());
            FirstResultCollector collector = new FirstResultCollector<>(limitOne);
            collectFindOne(session, preparedQuery, collector);
            return collector.result;
        });
    }

    @Override
    public  boolean exists(@NonNull PreparedQuery preparedQuery) {
        return findOne(preparedQuery) != null;
    }

    @NonNull
    @Override
    public  Iterable findAll(@NonNull PagedQuery pagedQuery) {
        return executeRead(session -> findPaged(session, pagedQuery));
    }

    @NonNull
    @Override
    public  Stream findStream(@NonNull PagedQuery pagedQuery) {
        return executeRead(session -> {
            StreamResultCollector collector = new StreamResultCollector<>();
            collectPagedResults(sessionFactory.getCriteriaBuilder(), session, pagedQuery, collector);
            return collector.result;
        });
    }

    @Override
    public  Page findPage(@NonNull PagedQuery pagedQuery) {
        return executeRead(session -> Page.of(
            findPaged(session, pagedQuery),
            pagedQuery.getPageable(),
            countOf(session, pagedQuery, pagedQuery.getPageable())
        ));
    }

    @Override
    public  long count(PagedQuery pagedQuery) {
        return executeRead(session -> countOf(session, pagedQuery, null));
    }

    private  List findPaged(Session session, PagedQuery pagedQuery) {
        ListResultCollector collector = new ListResultCollector<>();
        collectPagedResults(sessionFactory.getCriteriaBuilder(), session, pagedQuery, collector);
        return collector.result;
    }

    private  Long countOf(Session session, PagedQuery pagedQuery, @Nullable Pageable pageable) {
        SingleResultCollector collector = new SingleResultCollector<>();
        collectCountOf(sessionFactory.getCriteriaBuilder(), session, pagedQuery.getRootEntity(), pageable, collector);
        return collector.result;
    }

    @NonNull
    @Override
    public  Iterable findAll(@NonNull PreparedQuery preparedQuery) {
        return executeRead(session -> {
            ListResultCollector resultCollector = new ListResultCollector<>();
            collectFindAll(session, preparedQuery, resultCollector);
            return resultCollector.result;
        });
    }

    @Override
    public  T persist(@NonNull InsertOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        return executeWrite(session -> {
            if (storedQuery != null) {
                return executeUpdate(operation, session, storedQuery);
            }
            T entity = operation.getEntity();
            session.persist(entity);
            flushIfNecessary(session, operation.getAnnotationMetadata());
            return entity;
        });
    }

    @NonNull
    @Override
    public  T update(@NonNull UpdateOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        return executeWrite(session -> {
            if (storedQuery != null) {
                return executeUpdate(operation, session, storedQuery);
            }
            T entity = operation.getEntity();
            entity = session.merge(entity);
            flushIfNecessary(session, operation.getAnnotationMetadata());
            return entity;
        });
    }

    private  T executeUpdate(EntityInstanceOperation operation, Session session, StoredQuery storedQuery) {
        executeUpdate(session, storedQuery, operation.getInvocationContext(), operation.getEntity());
        if (flushIfNecessary(session, operation.getAnnotationMetadata())) {
            session.remove(operation.getEntity());
        }
        return operation.getEntity();
    }

    @NonNull
    @Override
    public  Iterable updateAll(@NonNull UpdateBatchOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        return executeWrite(session -> {
            if (storedQuery != null) {
                return executeUpdate(operation, session, storedQuery);
            }
            List results = new ArrayList<>();
            for (T entity : operation) {
                T merge = session.merge(entity);
                results.add(merge);
            }
            flushIfNecessary(session, operation.getAnnotationMetadata());
            return results;
        });
    }

    private  BatchOperation executeUpdate(BatchOperation operation, Session session, StoredQuery storedQuery) {
        for (T entity : operation) {
            executeUpdate(session, storedQuery, operation.getInvocationContext(), entity);
        }
        if (flushIfNecessary(session, operation.getAnnotationMetadata())) {
            for (T entity : operation) {
                session.remove(entity);
            }
        }
        return operation;
    }

    @NonNull
    @Override
    public  Iterable persistAll(@NonNull InsertBatchOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        return executeWrite(session -> {
            if (storedQuery != null) {
                return executeUpdate(operation, session, storedQuery);
            }
            for (T entity : operation) {
                session.persist(entity);
            }
            flushIfNecessary(session, operation.getAnnotationMetadata());
            return operation;
        });
    }

    private boolean flushIfNecessary(EntityManager entityManager, AnnotationMetadata annotationMetadata) {
        return flushIfNecessary(entityManager, annotationMetadata, false);
    }

    private boolean flushIfNecessary(EntityManager entityManager, AnnotationMetadata annotationMetadata, boolean clear) {
        if (annotationMetadata.hasAnnotation(QueryHint.class)) {
            FlushModeType flushModeType = getFlushModeType(annotationMetadata);
            if (flushModeType == FlushModeType.AUTO) {
                entityManager.flush();
                if (clear) {
                    entityManager.clear();
                }
                return true;
            }
        }
        return false;
    }

    @NonNull
    @Override
    public Optional executeUpdate(@NonNull PreparedQuery preparedQuery) {
        return executeWrite(session -> {
            String query = preparedQuery.getQuery();
            MutationQuery q = preparedQuery.isNative() ? session.createNativeMutationQuery(query) : session.createMutationQuery(query);
            bindParameters(q, preparedQuery, true);
            int numAffected = q.executeUpdate();
            flushIfNecessary(session, preparedQuery.getAnnotationMetadata(), true);
            return Optional.of(numAffected);
        });
    }

    @Override
    public  List execute(PreparedQuery preparedQuery) {
        return executeWrite(session -> {
            boolean needsOutRegistered = false;
            if (preparedQuery.isProcedure()) {
                Optional named = preparedQuery.getAnnotationMetadata().stringValue(Procedure.class, "named");
                ProcedureCall procedureQuery;
                if (named.isPresent()) {
                    procedureQuery = session.createNamedStoredProcedureQuery(named.get());
                } else {
                    String procedureName = preparedQuery.getAnnotationMetadata().stringValue(Procedure.class).orElseGet(preparedQuery::getName);
                    if (preparedQuery.getResultArgument().isVoid()) {
                        procedureQuery = session.createStoredProcedureQuery(procedureName);
                    } else {
                        procedureQuery = session.createStoredProcedureQuery(
                                procedureName,
                                preparedQuery.getResultArgument().getType()
                        );
                        needsOutRegistered = true;
                    }
                    int index = 1;
                    for (QueryParameterBinding queryBinding : preparedQuery.getQueryBindings()) {
                        int parameterIndex = queryBinding.getParameterIndex();
                        Argument argument = preparedQuery.getArguments()[parameterIndex];
                        procedureQuery.registerStoredProcedureParameter(
                                index++,
                                argument.getType(),
                                ParameterMode.IN);
                    }
                    if (needsOutRegistered) {
                        procedureQuery.registerStoredProcedureParameter(
                                index,
                                preparedQuery.getResultArgument().getType(),
                                ParameterMode.OUT);
                    }
                }
                boolean bindNamed = procedureQuery.getRegisteredParameters().stream().anyMatch(p -> p.getName() != null);
                bindParameters(procedureQuery, preparedQuery, bindNamed);
                procedureQuery.execute();
                if (preparedQuery.getResultArgument().isVoid()) {
                    flushIfNecessary(session, preparedQuery.getAnnotationMetadata(), true);
                    return List.of();
                }
                jakarta.persistence.Parameter procedureParameter = procedureQuery.getRegisteredParameters().stream().filter(p -> p.getMode() == ParameterMode.OUT)
                        .findFirst()
                        .orElseThrow(() -> new IllegalStateException("Cannot determine the output parameter!"));
                Object result;
                if (bindNamed) {
                    result = procedureQuery.getOutputParameterValue(procedureParameter.getName());
                } else {
                    result = procedureQuery.getOutputParameterValue(preparedQuery.getQueryBindings().size() + 1);
                }
                return List.of((R) result);
            } else {
                if (preparedQuery.isNative()) {
                    Iterable result = findAll(preparedQuery);
                    return (List) result;
                }
                throw new IllegalStateException("Only native query supports update RETURNING operations.");
            }
        });
    }

    @Override
    public  int delete(@NonNull DeleteOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        return executeWrite(session -> {
            if (storedQuery != null) {
                int numAffected = executeUpdate(session, storedQuery, operation.getInvocationContext(), operation.getEntity());
                if (flushIfNecessary(session, operation.getAnnotationMetadata())) {
                    session.remove(operation.getEntity());
                }
                return numAffected;
            }
            session.remove(operation.getEntity());
            return 1;
        });
    }

    @Override
    public  Optional deleteAll(@NonNull DeleteBatchOperation operation) {
        StoredQuery storedQuery = operation.getStoredQuery();
        Integer result = executeWrite(session -> {
            if (storedQuery != null) {
                int i = 0;
                for (T entity : operation) {
                    i += executeUpdate(session, storedQuery, operation.getInvocationContext(), entity);
                }
                if (flushIfNecessary(session, operation.getAnnotationMetadata())) {
                    for (T entity : operation) {
                        session.remove(entity);
                    }
                }
                return i;
            }
            int i = 0;
            for (T entity : operation) {
                session.remove(entity);
                i++;
            }
            return i;
        });
        return Optional.ofNullable(result);
    }

    private  int executeUpdate(Session session, StoredQuery storedQuery, InvocationContext invocationContext, T entity) {
        MutationQuery query = session.createMutationQuery(storedQuery.getQuery());
        bindParameters(query, storedQuery, invocationContext, true, entity);
        return query.executeUpdate();
    }

    @NonNull
    @Override
    public  Stream findStream(@NonNull PreparedQuery preparedQuery) {
        return executeRead(session -> {
            StreamResultCollector resultCollector = new StreamResultCollector<>();
            collectFindAll(session, preparedQuery, resultCollector);
            return resultCollector.result;
        });
    }

    private  R executeRead(Function callback) {
        return transactionOperations.executeRead(status -> callback.apply(getCurrentSession()));
    }

    private  R executeWrite(Function callback) {
        return transactionOperations.executeWrite(status -> callback.apply(getCurrentSession()));
    }

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @NonNull
    private ExecutorService newLocalThreadPool() {
        this.executorService = Executors.newCachedThreadPool();
        return executorService;
    }

    @NonNull
    @Override
    public ExecutorAsyncOperations async() {
        ExecutorAsyncOperations executorAsyncOperations = this.asyncOperations;
        if (executorAsyncOperations == null) {
            synchronized (this) { // double check
                executorAsyncOperations = this.asyncOperations;
                if (executorAsyncOperations == null) {
                    executorAsyncOperations = new ExecutorAsyncOperationsSupportingCriteria(
                        this,
                        this,
                        executorService != null ? executorService : newLocalThreadPool()
                    );
                    this.asyncOperations = executorAsyncOperations;
                }
            }
        }
        return executorAsyncOperations;
    }

    @NonNull
    @Override
    public ReactiveRepositoryOperations reactive() {
        if (dataConversionService instanceof DataConversionService asDataConversionService) {
            return new ExecutorReactiveOperationsSupportingCriteria((ExecutorAsyncOperationsSupportingCriteria) async(), asDataConversionService);
        }
        return new ExecutorReactiveOperationsSupportingCriteria((ExecutorAsyncOperationsSupportingCriteria) async(), null);
    }

    @NonNull
    @Override
    public EntityManager getCurrentEntityManager() {
        return sessionFactory.getCurrentSession();
    }

    @NonNull
    @Override
    public EntityManagerFactory getEntityManagerFactory() {
        return this.sessionFactory;
    }

    @Override
    public void flush() {
        executeWrite(session -> {
                session.flush();
                return null;
            }
        );
    }

    private boolean hasEntityGraph(AnnotationMetadata annotationMetadata) {
        return annotationMetadata.hasAnnotation(EntityGraph.class);
    }

    @Override
    public CriteriaBuilder getCriteriaBuilder() {
        return sessionFactory.getCriteriaBuilder();
    }

    @Override
    public  R findOne(CriteriaQuery query) {
        return executeRead(session -> session.createQuery(query).uniqueResult());
    }

    @Override
    public  List findAll(CriteriaQuery query) {
        return executeRead(session -> session.createQuery(query).getResultList());
    }

    @Override
    public  List findAll(CriteriaQuery query, int offset, int limit) {
        return executeRead(session -> {
            Query sessionQuery = session.createQuery(query);
            if (offset != -1) {
                sessionQuery = sessionQuery.setFirstResult(offset);
            }
            if (limit != -1) {
                sessionQuery = sessionQuery.setMaxResults(limit);
            }
            return sessionQuery.getResultList();
        });
    }

    @Override
    public Optional updateAll(CriteriaUpdate query) {
        return Optional.ofNullable(executeWrite(session -> session.createMutationQuery(query).executeUpdate()));
    }

    @Override
    public Optional deleteAll(CriteriaDelete query) {
        return Optional.ofNullable(executeWrite(session -> session.createMutationQuery(query).executeUpdate()));
    }

    private final class ListResultCollector extends ResultCollector {

        private List result;

        @Override
        protected void collectTuple(Query query, Function fn) {
            result = ((List) query.getResultList()).stream().map(fn).toList();
        }

        @Override
        protected void collect(Query query) {
            result = (List) query.getResultList();
        }
    }

    private final class StreamResultCollector extends ResultCollector {

        private Stream result;

        @Override
        protected void collectTuple(Query query, Function fn) {
            result = ((Stream) query.getResultStream()).map(fn);
        }

        @Override
        protected void collect(Query query) {
            result = (Stream) query.getResultStream();
        }
    }

    private final class SingleResultCollector extends ResultCollector {

        private R result;

        @Override
        protected void collectTuple(Query query, Function fn) {
            Tuple tuple = (Tuple) query.getSingleResult();
            if (tuple != null) {
                this.result = fn.apply(tuple);
            }
        }

        @Override
        protected void collect(Query query) {
            result = (R) query.getSingleResult();
        }
    }

    private final class FirstResultCollector extends ResultCollector {

        private final boolean limitOne;
        private R result;

        private FirstResultCollector(boolean limitOne) {
            this.limitOne = limitOne;
        }

        @Override
        protected void collectTuple(Query query, Function fn) {
            Tuple tuple = getFirst(query);
            if (tuple != null) {
                this.result = fn.apply(tuple);
            }
        }

        @Override
        protected void collect(Query query) {
            result = getFirst(query);
        }

        private  T getFirst(Query q) {
            if (limitOne) {
                q.setMaxResults(1);
            }
            Iterator iterator = (Iterator) q.getResultList().iterator();
            if (iterator.hasNext()) {
                return iterator.next();
            }
            return null;
        }
    }

}