org.hibernate.internal.SessionImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.hibernate.internal.SessionImpl.java

Source

/*
 * 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.internal;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.NClob;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import javax.persistence.CacheRetrieveMode;
import javax.persistence.CacheStoreMode;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TransactionRequiredException;
import javax.persistence.criteria.CriteriaBuilder;

import org.hibernate.CacheMode;
import org.hibernate.Criteria;
import org.hibernate.Filter;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.IdentifierLoadAccess;
import org.hibernate.JDBCException;
import org.hibernate.LobHelper;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.MultiIdentifierLoadAccess;
import org.hibernate.NaturalIdLoadAccess;
import org.hibernate.ObjectDeletedException;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.QueryException;
import org.hibernate.ReplicationMode;
import org.hibernate.ScrollMode;
import org.hibernate.Session;
import org.hibernate.SessionEventListener;
import org.hibernate.SessionException;
import org.hibernate.SharedSessionBuilder;
import org.hibernate.SimpleNaturalIdLoadAccess;
import org.hibernate.Transaction;
import org.hibernate.TransientObjectException;
import org.hibernate.TypeHelper;
import org.hibernate.TypeMismatchException;
import org.hibernate.UnknownProfileException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.criterion.NaturalIdentifier;
import org.hibernate.engine.internal.StatefulPersistenceContext;
import org.hibernate.engine.jdbc.LobCreator;
import org.hibernate.engine.jdbc.NonContextualLobCreator;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.query.spi.FilterQueryPlan;
import org.hibernate.engine.query.spi.HQLQueryPlan;
import org.hibernate.engine.query.spi.NativeSQLQueryPlan;
import org.hibernate.engine.query.spi.sql.NativeSQLQuerySpecification;
import org.hibernate.engine.spi.ActionQueue;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.EffectiveEntityGraph;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.NamedQueryDefinition;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.engine.transaction.spi.TransactionImplementor;
import org.hibernate.engine.transaction.spi.TransactionObserver;
import org.hibernate.event.spi.AutoFlushEvent;
import org.hibernate.event.spi.AutoFlushEventListener;
import org.hibernate.event.spi.ClearEvent;
import org.hibernate.event.spi.ClearEventListener;
import org.hibernate.event.spi.DeleteEvent;
import org.hibernate.event.spi.DeleteEventListener;
import org.hibernate.event.spi.DirtyCheckEvent;
import org.hibernate.event.spi.DirtyCheckEventListener;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EvictEvent;
import org.hibernate.event.spi.EvictEventListener;
import org.hibernate.event.spi.FlushEvent;
import org.hibernate.event.spi.FlushEventListener;
import org.hibernate.event.spi.InitializeCollectionEvent;
import org.hibernate.event.spi.InitializeCollectionEventListener;
import org.hibernate.event.spi.LoadEvent;
import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.event.spi.LoadEventListener.LoadType;
import org.hibernate.event.spi.LockEvent;
import org.hibernate.event.spi.LockEventListener;
import org.hibernate.event.spi.MergeEvent;
import org.hibernate.event.spi.MergeEventListener;
import org.hibernate.event.spi.PersistEvent;
import org.hibernate.event.spi.PersistEventListener;
import org.hibernate.event.spi.RefreshEvent;
import org.hibernate.event.spi.RefreshEventListener;
import org.hibernate.event.spi.ReplicateEvent;
import org.hibernate.event.spi.ReplicateEventListener;
import org.hibernate.event.spi.ResolveNaturalIdEvent;
import org.hibernate.event.spi.ResolveNaturalIdEventListener;
import org.hibernate.event.spi.SaveOrUpdateEvent;
import org.hibernate.event.spi.SaveOrUpdateEventListener;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.RootGraph;
import org.hibernate.graph.internal.RootGraphImpl;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.hql.spi.QueryTranslator;
import org.hibernate.internal.CriteriaImpl.CriterionEntry;
import org.hibernate.internal.log.DeprecationLogger;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.jdbc.Work;
import org.hibernate.jdbc.WorkExecutor;
import org.hibernate.jdbc.WorkExecutorVisitable;
import org.hibernate.jpa.AvailableSettings;
import org.hibernate.jpa.QueryHints;
import org.hibernate.jpa.internal.util.CacheModeHelper;
import org.hibernate.jpa.internal.util.ConfigurationHelper;
import org.hibernate.jpa.internal.util.FlushModeTypeHelper;
import org.hibernate.jpa.internal.util.LockModeTypeHelper;
import org.hibernate.jpa.internal.util.LockOptionsHelper;
import org.hibernate.jpa.spi.HibernateEntityManagerImplementor;
import org.hibernate.loader.criteria.CriteriaLoader;
import org.hibernate.loader.custom.CustomLoader;
import org.hibernate.loader.custom.CustomQuery;
import org.hibernate.metamodel.spi.MetamodelImplementor;
import org.hibernate.param.CollectionFilterKeyParameterSpecification;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.MultiLoadOptions;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.procedure.ProcedureCall;
import org.hibernate.procedure.ProcedureCallMemento;
import org.hibernate.procedure.UnknownSqlResultSetMappingException;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.Query;
import org.hibernate.query.internal.CollectionFilterImpl;
import org.hibernate.query.spi.ScrollableResultsImplementor;
import org.hibernate.resource.transaction.TransactionRequiredForJoinException;
import org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorImpl;
import org.hibernate.resource.transaction.spi.TransactionCoordinator;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.hibernate.stat.SessionStatistics;
import org.hibernate.stat.internal.SessionStatisticsImpl;
import org.hibernate.stat.spi.StatisticsImplementor;

import static org.hibernate.cfg.AvailableSettings.JPA_LOCK_SCOPE;
import static org.hibernate.cfg.AvailableSettings.JPA_LOCK_TIMEOUT;
import static org.hibernate.cfg.AvailableSettings.JPA_SHARED_CACHE_RETRIEVE_MODE;
import static org.hibernate.cfg.AvailableSettings.JPA_SHARED_CACHE_STORE_MODE;

/**
 * Concrete implementation of a Session.
 * <p/>
 * Exposes two interfaces:<ul>
 * <li>{@link org.hibernate.Session} to the application</li>
 * <li>{@link org.hibernate.engine.spi.SessionImplementor} to other Hibernate components (SPI)</li>
 * </ul>
 * <p/>
 * This class is not thread-safe.
 *
 * @author Gavin King
 * @author Steve Ebersole
 * @author Brett Meyer
 * @author Chris Cranford
 * @author Sanne Grinovero
 */
public final class SessionImpl extends AbstractSessionImpl
        implements EventSource, SessionImplementor, HibernateEntityManagerImplementor {
    private static final EntityManagerMessageLogger log = HEMLogging.messageLogger(SessionImpl.class);

    // Defaults to null which means the properties are the default - as defined in FastSessionServices#defaultSessionProperties
    private Map<String, Object> properties;

    private transient ActionQueue actionQueue;
    private transient StatefulPersistenceContext persistenceContext;

    private transient LoadQueryInfluencers loadQueryInfluencers;

    private LockOptions lockOptions;

    private boolean autoClear;
    private boolean autoClose;
    private boolean queryParametersValidationEnabled;

    private transient int dontFlushFromFind;

    private transient LoadEvent loadEvent; //cached LoadEvent instance

    private transient TransactionObserver transactionObserver;

    public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) {
        super(factory, options);

        this.actionQueue = new ActionQueue(this);
        this.persistenceContext = new StatefulPersistenceContext(this);

        this.autoClear = options.shouldAutoClear();
        this.autoClose = options.shouldAutoClose();
        this.queryParametersValidationEnabled = options.isQueryParametersValidationEnabled();

        if (options instanceof SharedSessionCreationOptions) {
            final SharedSessionCreationOptions sharedOptions = (SharedSessionCreationOptions) options;
            final ActionQueue.TransactionCompletionProcesses transactionCompletionProcesses = sharedOptions
                    .getTransactionCompletionProcesses();
            if (sharedOptions.isTransactionCoordinatorShared() && transactionCompletionProcesses != null) {
                actionQueue.setTransactionCompletionProcesses(transactionCompletionProcesses, true);
            }
        }

        loadQueryInfluencers = new LoadQueryInfluencers(factory);

        final StatisticsImplementor statistics = factory.getStatistics();
        if (statistics.isStatisticsEnabled()) {
            statistics.openSession();
        }

        if (this.properties != null) {
            //There might be custom properties for this session that affect the LockOptions state
            LockOptionsHelper.applyPropertiesToLockOptions(this.properties, this::getLockOptionsForWrite);
        }
        getSession().setCacheMode(fastSessionServices.initialSessionCacheMode);

        // NOTE : pulse() already handles auto-join-ability correctly
        getTransactionCoordinator().pulse();

        if (log.isTraceEnabled()) {
            log.tracef("Opened Session [%s] at timestamp: %s", getSessionIdentifier(), getTimestamp());
        }
    }

    private LockOptions getLockOptionsForRead() {
        return this.lockOptions == null ? fastSessionServices.defaultLockOptions : this.lockOptions;
    }

    private LockOptions getLockOptionsForWrite() {
        if (this.lockOptions == null) {
            this.lockOptions = new LockOptions();
        }
        return this.lockOptions;
    }

    protected void applyQuerySettingsAndHints(Query query) {
        final LockOptions lockOptionsForRead = getLockOptionsForRead();
        if (lockOptionsForRead.getLockMode() != LockMode.NONE) {
            query.setLockMode(getLockMode(lockOptionsForRead.getLockMode()));
        }
        final Object queryTimeout;
        if ((queryTimeout = getSessionProperty(QueryHints.SPEC_HINT_TIMEOUT)) != null) {
            query.setHint(QueryHints.SPEC_HINT_TIMEOUT, queryTimeout);
        }
        final Object lockTimeout;
        if ((lockTimeout = getSessionProperty(JPA_LOCK_TIMEOUT)) != null) {
            query.setHint(JPA_LOCK_TIMEOUT, lockTimeout);
        }
    }

    private Object getSessionProperty(final String name) {
        if (properties == null) {
            return fastSessionServices.defaultSessionProperties.get(name);
        } else {
            return properties.get(name);
        }
    }

    @Override
    public SharedSessionBuilder sessionWithOptions() {
        return new SharedSessionBuilderImpl(this);
    }

    @Override
    public void clear() {
        checkOpen();

        // Do not call checkTransactionSynchStatus() here -- if a delayed
        // afterCompletion exists, it can cause an infinite loop.
        pulseTransactionCoordinator();

        try {
            internalClear();
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    private void internalClear() {
        persistenceContext.clear();
        actionQueue.clear();

        fastSessionServices.eventListenerGroup_CLEAR.fireLazyEventOnEachListener(this::createClearEvent,
                ClearEventListener::onClear);
    }

    private ClearEvent createClearEvent() {
        return new ClearEvent(this);
    }

    @Override
    @SuppressWarnings("StatementWithEmptyBody")
    public void close() throws HibernateException {
        if (isClosed()) {
            if (getFactory().getSessionFactoryOptions().getJpaCompliance().isJpaClosedComplianceEnabled()) {
                throw new IllegalStateException("Illegal call to #close() on already closed Session/EntityManager");
            }

            log.trace("Already closed");
            return;
        }

        closeWithoutOpenChecks();
    }

    public void closeWithoutOpenChecks() throws HibernateException {
        if (log.isTraceEnabled()) {
            log.tracef("Closing session [%s]", getSessionIdentifier());
        }

        // todo : we want this check if usage is JPA, but not native Hibernate usage
        final SessionFactoryImplementor sessionFactory = getSessionFactory();
        if (sessionFactory.getSessionFactoryOptions().isJpaBootstrap()) {
            // Original hibernate-entitymanager EM#close behavior
            checkSessionFactoryOpen();
            checkOpenOrWaitingForAutoClose();
            if (fastSessionServices.discardOnClose || !isTransactionInProgress(false)) {
                super.close();
            } else {
                //Otherwise, session auto-close will be enabled by shouldAutoCloseSession().
                waitingForAutoClose = true;
                closed = true;
            }
        } else {
            super.close();
        }

        final StatisticsImplementor statistics = sessionFactory.getStatistics();
        if (statistics.isStatisticsEnabled()) {
            statistics.closeSession();
        }
    }

    private boolean isTransactionInProgress(boolean isMarkedRollbackConsideredActive) {
        if (waitingForAutoClose) {
            return getSessionFactory().isOpen()
                    && getTransactionCoordinator().isTransactionActive(isMarkedRollbackConsideredActive);
        }
        return !isClosed() && getTransactionCoordinator().isTransactionActive(isMarkedRollbackConsideredActive);
    }

    @Override
    protected boolean shouldCloseJdbcCoordinatorOnClose(boolean isTransactionCoordinatorShared) {
        if (!isTransactionCoordinatorShared) {
            return super.shouldCloseJdbcCoordinatorOnClose(isTransactionCoordinatorShared);
        }

        final ActionQueue actionQueue = getActionQueue();
        if (actionQueue.hasBeforeTransactionActions() || actionQueue.hasAfterTransactionActions()) {
            log.warn(
                    "On close, shared Session had before/after transaction actions that have not yet been processed");
        }
        return false;
    }

    @Override
    public boolean isAutoCloseSessionEnabled() {
        return autoClose;
    }

    @Override
    public boolean isQueryParametersValidationEnabled() {
        return queryParametersValidationEnabled;
    }

    @Override
    public boolean isOpen() {
        checkSessionFactoryOpen();
        checkTransactionSynchStatus();
        try {
            return !isClosed();
        } catch (HibernateException he) {
            throw getExceptionConverter().convert(he);
        }
    }

    protected void checkSessionFactoryOpen() {
        if (!getFactory().isOpen()) {
            log.debug(
                    "Forcing Session/EntityManager closed as SessionFactory/EntityManagerFactory has been closed");
            setClosed();
        }
    }

    private void managedFlush() {
        if (isClosed() && !waitingForAutoClose) {
            log.trace("Skipping auto-flush due to session closed");
            return;
        }
        log.trace("Automatically flushing session");
        doFlush();
    }

    @Override
    public boolean shouldAutoClose() {
        if (waitingForAutoClose) {
            return true;
        } else if (isClosed()) {
            return false;
        } else {
            // JPA technically requires that this be a PersistentUnityTransactionType#JTA to work,
            // but we do not assert that here...
            //return isAutoCloseSessionEnabled() && getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta();
            return isAutoCloseSessionEnabled();
        }
    }

    private void managedClose() {
        log.trace("Automatically closing session");
        closeWithoutOpenChecks();
    }

    @Override
    public Connection connection() throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        return getJdbcCoordinator().getLogicalConnection().getPhysicalConnection();
    }

    @Override
    public Connection disconnect() throws HibernateException {
        checkOpen();
        log.debug("Disconnecting session");
        return getJdbcCoordinator().getLogicalConnection().manualDisconnect();
    }

    @Override
    public void reconnect(Connection conn) throws HibernateException {
        checkOpen();
        log.debug("Reconnecting session");
        checkTransactionSynchStatus();
        getJdbcCoordinator().getLogicalConnection().manualReconnect(conn);
    }

    @Override
    public void setAutoClear(boolean enabled) {
        checkOpenOrWaitingForAutoClose();
        autoClear = enabled;
    }

    /**
     * Check if there is a Hibernate or JTA transaction in progress and,
     * if there is not, flush if necessary, make sure the connection has
     * been committed (if it is not in autocommit mode) and run the after
     * completion processing
     *
     * @param success Was the operation a success
     */
    public void afterOperation(boolean success) {
        if (!isTransactionInProgress()) {
            getJdbcCoordinator().afterTransaction();
        }
    }

    @Override
    public void addEventListeners(SessionEventListener... listeners) {
        getEventListenerManager().addListener(listeners);
    }

    /**
     * clear all the internal collections, just
     * to help the garbage collector, does not
     * clear anything that is needed during the
     * afterTransactionCompletion() phase
     */
    @Override
    protected void cleanupOnClose() {
        persistenceContext.clear();
    }

    @Override
    public LockMode getCurrentLockMode(Object object) throws HibernateException {
        checkOpen();
        checkTransactionSynchStatus();
        if (object == null) {
            throw new NullPointerException("null object passed to getCurrentLockMode()");
        }
        if (object instanceof HibernateProxy) {
            object = ((HibernateProxy) object).getHibernateLazyInitializer().getImplementation(this);
            if (object == null) {
                return LockMode.NONE;
            }
        }
        EntityEntry e = persistenceContext.getEntry(object);
        if (e == null) {
            throw new TransientObjectException("Given object not associated with the session");
        }
        if (e.getStatus() != Status.MANAGED) {
            throw new ObjectDeletedException("The given object was deleted", e.getId(),
                    e.getPersister().getEntityName());
        }
        return e.getLockMode();
    }

    @Override
    public Object getEntityUsingInterceptor(EntityKey key) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        // todo : should this get moved to PersistentContext?
        // logically, is PersistentContext the "thing" to which an interceptor gets attached?
        final Object result = persistenceContext.getEntity(key);
        if (result == null) {
            final Object newObject = getInterceptor().getEntity(key.getEntityName(), key.getIdentifier());
            if (newObject != null) {
                lock(newObject, LockMode.NONE);
            }
            return newObject;
        } else {
            return result;
        }
    }

    private void checkNoUnresolvedActionsBeforeOperation() {
        if (persistenceContext.getCascadeLevel() == 0 && actionQueue.hasUnresolvedEntityInsertActions()) {
            throw new IllegalStateException(
                    "There are delayed insert actions before operation as cascade level 0.");
        }
    }

    private void checkNoUnresolvedActionsAfterOperation() {
        if (persistenceContext.getCascadeLevel() == 0) {
            actionQueue.checkNoUnresolvedActionsAfterOperation();
        }
        delayedAfterCompletion();
    }

    @Override
    protected void delayedAfterCompletion() {
        if (getTransactionCoordinator() instanceof JtaTransactionCoordinatorImpl) {
            ((JtaTransactionCoordinatorImpl) getTransactionCoordinator()).getSynchronizationCallbackCoordinator()
                    .processAnyDelayedAfterCompletion();
        }
    }

    // saveOrUpdate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void saveOrUpdate(Object object) throws HibernateException {
        saveOrUpdate(null, object);
    }

    @Override
    public void saveOrUpdate(String entityName, Object obj) throws HibernateException {
        fireSaveOrUpdate(new SaveOrUpdateEvent(entityName, obj, this));
    }

    private void fireSaveOrUpdate(final SaveOrUpdateEvent event) {
        checkOpen();
        checkTransactionSynchStatus();
        checkNoUnresolvedActionsBeforeOperation();
        fastSessionServices.eventListenerGroup_SAVE_UPDATE.fireEventOnEachListener(event,
                SaveOrUpdateEventListener::onSaveOrUpdate);
        checkNoUnresolvedActionsAfterOperation();
    }

    // save() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public Serializable save(Object obj) throws HibernateException {
        return save(null, obj);
    }

    @Override
    public Serializable save(String entityName, Object object) throws HibernateException {
        return fireSave(new SaveOrUpdateEvent(entityName, object, this));
    }

    private Serializable fireSave(final SaveOrUpdateEvent event) {
        checkOpen();
        checkTransactionSynchStatus();
        checkNoUnresolvedActionsBeforeOperation();
        fastSessionServices.eventListenerGroup_SAVE.fireEventOnEachListener(event,
                SaveOrUpdateEventListener::onSaveOrUpdate);
        checkNoUnresolvedActionsAfterOperation();
        return event.getResultId();
    }

    // update() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void update(Object obj) throws HibernateException {
        update(null, obj);
    }

    @Override
    public void update(String entityName, Object object) throws HibernateException {
        fireUpdate(new SaveOrUpdateEvent(entityName, object, this));
    }

    private void fireUpdate(SaveOrUpdateEvent event) {
        checkOpen();
        checkTransactionSynchStatus();
        checkNoUnresolvedActionsBeforeOperation();
        fastSessionServices.eventListenerGroup_UPDATE.fireEventOnEachListener(event,
                SaveOrUpdateEventListener::onSaveOrUpdate);
        checkNoUnresolvedActionsAfterOperation();
    }

    // lock() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void lock(String entityName, Object object, LockMode lockMode) throws HibernateException {
        fireLock(new LockEvent(entityName, object, lockMode, this));
    }

    @Override
    public LockRequest buildLockRequest(LockOptions lockOptions) {
        return new LockRequestImpl(lockOptions);
    }

    @Override
    public void lock(Object object, LockMode lockMode) throws HibernateException {
        fireLock(new LockEvent(object, lockMode, this));
    }

    private void fireLock(String entityName, Object object, LockOptions options) {
        fireLock(new LockEvent(entityName, object, options, this));
    }

    private void fireLock(Object object, LockOptions options) {
        fireLock(new LockEvent(object, options, this));
    }

    private void fireLock(LockEvent event) {
        checkOpen();
        pulseTransactionCoordinator();
        fastSessionServices.eventListenerGroup_LOCK.fireEventOnEachListener(event, LockEventListener::onLock);
        delayedAfterCompletion();
    }

    // persist() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void persist(String entityName, Object object) throws HibernateException {
        checkOpen();
        firePersist(new PersistEvent(entityName, object, this));
    }

    @Override
    public void persist(Object object) throws HibernateException {
        checkOpen();
        firePersist(new PersistEvent(null, object, this));
    }

    @Override
    public void persist(String entityName, Object object, Map copiedAlready) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        firePersist(copiedAlready, new PersistEvent(entityName, object, this));
    }

    private void firePersist(final PersistEvent event) {
        try {
            checkTransactionSynchStatus();
            checkNoUnresolvedActionsBeforeOperation();

            fastSessionServices.eventListenerGroup_PERSIST.fireEventOnEachListener(event,
                    PersistEventListener::onPersist);
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage()));
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        } finally {
            try {
                checkNoUnresolvedActionsAfterOperation();
            } catch (RuntimeException e) {
                throw getExceptionConverter().convert(e);
            }
        }
    }

    private void firePersist(final Map copiedAlready, final PersistEvent event) {
        pulseTransactionCoordinator();

        try {
            //Uses a capturing lambda in this case as we need to carry the additional Map parameter:
            fastSessionServices.eventListenerGroup_PERSIST.fireEventOnEachListener(event, copiedAlready,
                    PersistEventListener::onPersist);
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage()));
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    // persistOnFlush() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void persistOnFlush(String entityName, Object object, Map copiedAlready) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        PersistEvent event = new PersistEvent(entityName, object, this);
        fastSessionServices.eventListenerGroup_PERSIST_ONFLUSH.fireEventOnEachListener(event, copiedAlready,
                PersistEventListener::onPersist);
        delayedAfterCompletion();
    }

    // merge() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public Object merge(String entityName, Object object) throws HibernateException {
        checkOpen();
        return fireMerge(new MergeEvent(entityName, object, this));
    }

    @Override
    public Object merge(Object object) throws HibernateException {
        checkOpen();
        return fireMerge(new MergeEvent(null, object, this));
    }

    @Override
    public void merge(String entityName, Object object, Map copiedAlready) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        fireMerge(copiedAlready, new MergeEvent(entityName, object, this));
    }

    private Object fireMerge(MergeEvent event) {
        try {
            checkTransactionSynchStatus();
            checkNoUnresolvedActionsBeforeOperation();
            fastSessionServices.eventListenerGroup_MERGE.fireEventOnEachListener(event,
                    MergeEventListener::onMerge);
            checkNoUnresolvedActionsAfterOperation();
        } catch (ObjectDeletedException sse) {
            throw getExceptionConverter().convert(new IllegalArgumentException(sse));
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            //including HibernateException
            throw getExceptionConverter().convert(e);
        }

        return event.getResult();
    }

    private void fireMerge(final Map copiedAlready, final MergeEvent event) {
        try {
            pulseTransactionCoordinator();
            fastSessionServices.eventListenerGroup_MERGE.fireEventOnEachListener(event, copiedAlready,
                    MergeEventListener::onMerge);
        } catch (ObjectDeletedException sse) {
            throw getExceptionConverter().convert(new IllegalArgumentException(sse));
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            //including HibernateException
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    // delete() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void delete(Object object) throws HibernateException {
        checkOpen();
        fireDelete(new DeleteEvent(object, this));
    }

    @Override
    public void delete(String entityName, Object object) throws HibernateException {
        checkOpen();
        fireDelete(new DeleteEvent(entityName, object, this));
    }

    @Override
    public void delete(String entityName, Object object, boolean isCascadeDeleteEnabled, Set transientEntities)
            throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        final boolean removingOrphanBeforeUpates = persistenceContext.isRemovingOrphanBeforeUpates();
        final boolean traceEnabled = log.isTraceEnabled();
        if (traceEnabled && removingOrphanBeforeUpates) {
            logRemoveOrphanBeforeUpdates("before continuing", entityName, object);
        }
        fireDelete(new DeleteEvent(entityName, object, isCascadeDeleteEnabled, removingOrphanBeforeUpates, this),
                transientEntities);
        if (traceEnabled && removingOrphanBeforeUpates) {
            logRemoveOrphanBeforeUpdates("after continuing", entityName, object);
        }
    }

    @Override
    public void removeOrphanBeforeUpdates(String entityName, Object child) {
        // TODO: The removeOrphan concept is a temporary "hack" for HHH-6484.  This should be removed once action/task
        // ordering is improved.
        final boolean traceEnabled = log.isTraceEnabled();
        if (traceEnabled) {
            logRemoveOrphanBeforeUpdates("begin", entityName, child);
        }
        persistenceContext.beginRemoveOrphanBeforeUpdates();
        try {
            checkOpenOrWaitingForAutoClose();
            fireDelete(new DeleteEvent(entityName, child, false, true, this));
        } finally {
            persistenceContext.endRemoveOrphanBeforeUpdates();
            if (traceEnabled) {
                logRemoveOrphanBeforeUpdates("end", entityName, child);
            }
        }
    }

    private void logRemoveOrphanBeforeUpdates(String timing, String entityName, Object entity) {
        if (log.isTraceEnabled()) {
            final EntityEntry entityEntry = persistenceContext.getEntry(entity);
            log.tracef("%s remove orphan before updates: [%s]", timing,
                    entityEntry == null ? entityName : MessageHelper.infoString(entityName, entityEntry.getId()));
        }
    }

    private void fireDelete(final DeleteEvent event) {
        try {
            pulseTransactionCoordinator();
            fastSessionServices.eventListenerGroup_DELETE.fireEventOnEachListener(event,
                    DeleteEventListener::onDelete);
        } catch (ObjectDeletedException sse) {
            throw getExceptionConverter().convert(new IllegalArgumentException(sse));
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            //including HibernateException
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    private void fireDelete(final DeleteEvent event, final Set transientEntities) {
        try {
            pulseTransactionCoordinator();
            fastSessionServices.eventListenerGroup_DELETE.fireEventOnEachListener(event, transientEntities,
                    DeleteEventListener::onDelete);
        } catch (ObjectDeletedException sse) {
            throw getExceptionConverter().convert(new IllegalArgumentException(sse));
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            //including HibernateException
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    // load()/get() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void load(Object object, Serializable id) throws HibernateException {
        LoadEvent event = loadEvent;
        loadEvent = null;
        if (event == null) {
            event = new LoadEvent(id, object, this);
        } else {
            event.setEntityClassName(null);
            event.setEntityId(id);
            event.setInstanceToLoad(object);
            event.setLockMode(LoadEvent.DEFAULT_LOCK_MODE);
            event.setLockScope(LoadEvent.DEFAULT_LOCK_OPTIONS.getScope());
            event.setLockTimeout(LoadEvent.DEFAULT_LOCK_OPTIONS.getTimeOut());
        }

        fireLoad(event, LoadEventListener.RELOAD);

        if (loadEvent == null) {
            event.setEntityClassName(null);
            event.setEntityId(null);
            event.setInstanceToLoad(null);
            event.setResult(null);
            loadEvent = event;
        }
    }

    @Override
    public <T> T load(Class<T> entityClass, Serializable id) throws HibernateException {
        return this.byId(entityClass).getReference(id);
    }

    @Override
    public Object load(String entityName, Serializable id) throws HibernateException {
        return this.byId(entityName).getReference(id);
    }

    @Override
    public <T> T get(Class<T> entityClass, Serializable id) throws HibernateException {
        return this.byId(entityClass).load(id);
    }

    @Override
    public Object get(String entityName, Serializable id) throws HibernateException {
        return this.byId(entityName).load(id);
    }

    /**
     * Load the data for the object with the specified id into a newly created object.
     * This is only called when lazily initializing a proxy.
     * Do NOT return a proxy.
     */
    @Override
    public Object immediateLoad(String entityName, Serializable id) throws HibernateException {
        if (log.isDebugEnabled()) {
            EntityPersister persister = getFactory().getMetamodel().entityPersister(entityName);
            log.debugf("Initializing proxy: %s", MessageHelper.infoString(persister, id, getFactory()));
        }
        LoadEvent event = loadEvent;
        loadEvent = null;
        event = recycleEventInstance(event, id, entityName);
        fireLoadNoChecks(event, LoadEventListener.IMMEDIATE_LOAD);
        Object result = event.getResult();
        if (loadEvent == null) {
            event.setEntityClassName(null);
            event.setEntityId(null);
            event.setInstanceToLoad(null);
            event.setResult(null);
            loadEvent = event;
        }
        return result;
    }

    @Override
    public final Object internalLoad(String entityName, Serializable id, boolean eager, boolean nullable) {
        final EffectiveEntityGraph effectiveEntityGraph = getLoadQueryInfluencers().getEffectiveEntityGraph();
        final GraphSemantic semantic = effectiveEntityGraph.getSemantic();
        final RootGraphImplementor<?> graph = effectiveEntityGraph.getGraph();
        boolean clearedEffectiveGraph = false;
        if (semantic != null) {
            if (!graph.appliesTo(entityName)) {
                log.debug("Clearing effective entity graph for subsequent-select");
                clearedEffectiveGraph = true;
                effectiveEntityGraph.clear();
            }
        }

        try {
            final LoadEventListener.LoadType type;
            if (nullable) {
                type = LoadEventListener.INTERNAL_LOAD_NULLABLE;
            } else {
                type = eager ? LoadEventListener.INTERNAL_LOAD_EAGER : LoadEventListener.INTERNAL_LOAD_LAZY;
            }

            LoadEvent event = loadEvent;
            loadEvent = null;

            event = recycleEventInstance(event, id, entityName);

            fireLoadNoChecks(event, type);

            Object result = event.getResult();

            if (!nullable) {
                UnresolvableObjectException.throwIfNull(result, id, entityName);
            }

            if (loadEvent == null) {
                event.setEntityClassName(null);
                event.setEntityId(null);
                event.setInstanceToLoad(null);
                event.setResult(null);
                loadEvent = event;
            }
            return result;
        } finally {
            if (clearedEffectiveGraph) {
                effectiveEntityGraph.applyGraph(graph, semantic);
            }
        }
    }

    /**
     * Helper to avoid creating many new instances of LoadEvent: it's an allocation hot spot.
     */
    private LoadEvent recycleEventInstance(final LoadEvent event, final Serializable id, final String entityName) {
        if (event == null) {
            return new LoadEvent(id, entityName, true, this);
        } else {
            event.setEntityClassName(entityName);
            event.setEntityId(id);
            event.setInstanceToLoad(null);
            event.setLockMode(LoadEvent.DEFAULT_LOCK_MODE);
            event.setLockScope(LoadEvent.DEFAULT_LOCK_OPTIONS.getScope());
            event.setLockTimeout(LoadEvent.DEFAULT_LOCK_OPTIONS.getTimeOut());
            return event;
        }
    }

    @Override
    public <T> T load(Class<T> entityClass, Serializable id, LockMode lockMode) throws HibernateException {
        return this.byId(entityClass).with(new LockOptions(lockMode)).getReference(id);
    }

    @Override
    public <T> T load(Class<T> entityClass, Serializable id, LockOptions lockOptions) throws HibernateException {
        return this.byId(entityClass).with(lockOptions).getReference(id);
    }

    @Override
    public Object load(String entityName, Serializable id, LockMode lockMode) throws HibernateException {
        return this.byId(entityName).with(new LockOptions(lockMode)).getReference(id);
    }

    @Override
    public Object load(String entityName, Serializable id, LockOptions lockOptions) throws HibernateException {
        return this.byId(entityName).with(lockOptions).getReference(id);
    }

    @Override
    public <T> T get(Class<T> entityClass, Serializable id, LockMode lockMode) throws HibernateException {
        return this.byId(entityClass).with(new LockOptions(lockMode)).load(id);
    }

    @Override
    public <T> T get(Class<T> entityClass, Serializable id, LockOptions lockOptions) throws HibernateException {
        return this.byId(entityClass).with(lockOptions).load(id);
    }

    @Override
    public Object get(String entityName, Serializable id, LockMode lockMode) throws HibernateException {
        return this.byId(entityName).with(new LockOptions(lockMode)).load(id);
    }

    @Override
    public Object get(String entityName, Serializable id, LockOptions lockOptions) throws HibernateException {
        return this.byId(entityName).with(lockOptions).load(id);
    }

    @Override
    public IdentifierLoadAccessImpl byId(String entityName) {
        return new IdentifierLoadAccessImpl(entityName);
    }

    @Override
    public <T> IdentifierLoadAccessImpl<T> byId(Class<T> entityClass) {
        return new IdentifierLoadAccessImpl<T>(entityClass);
    }

    @Override
    public <T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass) {
        return new MultiIdentifierLoadAccessImpl<T>(locateEntityPersister(entityClass));
    }

    @Override
    public MultiIdentifierLoadAccess byMultipleIds(String entityName) {
        return new MultiIdentifierLoadAccessImpl(locateEntityPersister(entityName));
    }

    @Override
    public NaturalIdLoadAccess byNaturalId(String entityName) {
        return new NaturalIdLoadAccessImpl(entityName);
    }

    @Override
    public <T> NaturalIdLoadAccess<T> byNaturalId(Class<T> entityClass) {
        return new NaturalIdLoadAccessImpl<T>(entityClass);
    }

    @Override
    public SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName) {
        return new SimpleNaturalIdLoadAccessImpl(entityName);
    }

    @Override
    public <T> SimpleNaturalIdLoadAccess<T> bySimpleNaturalId(Class<T> entityClass) {
        return new SimpleNaturalIdLoadAccessImpl<T>(entityClass);
    }

    private void fireLoad(LoadEvent event, LoadType loadType) {
        checkOpenOrWaitingForAutoClose();
        fireLoadNoChecks(event, loadType);
        delayedAfterCompletion();
    }

    //Performance note:
    // This version of #fireLoad is meant to be invoked by internal methods only,
    // so to skip the session open, transaction synch, etc.. checks,
    // which have been proven to be not particularly cheap:
    // it seems they prevent these hot methods from being inlined.
    private void fireLoadNoChecks(final LoadEvent event, final LoadType loadType) {
        pulseTransactionCoordinator();
        fastSessionServices.eventListenerGroup_LOAD.fireEventOnEachListener(event, loadType,
                LoadEventListener::onLoad);
    }

    private void fireResolveNaturalId(final ResolveNaturalIdEvent event) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        fastSessionServices.eventListenerGroup_RESOLVE_NATURAL_ID.fireEventOnEachListener(event,
                ResolveNaturalIdEventListener::onResolveNaturalId);
        delayedAfterCompletion();
    }

    // refresh() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void refresh(Object object) throws HibernateException {
        checkOpen();
        fireRefresh(new RefreshEvent(null, object, this));
    }

    @Override
    public void refresh(String entityName, Object object) throws HibernateException {
        checkOpen();
        fireRefresh(new RefreshEvent(entityName, object, this));
    }

    @Override
    public void refresh(Object object, LockMode lockMode) throws HibernateException {
        checkOpen();
        fireRefresh(new RefreshEvent(object, lockMode, this));
    }

    @Override
    public void refresh(Object object, LockOptions lockOptions) throws HibernateException {
        checkOpen();
        refresh(null, object, lockOptions);
    }

    @Override
    public void refresh(String entityName, Object object, LockOptions lockOptions) throws HibernateException {
        checkOpen();
        fireRefresh(new RefreshEvent(entityName, object, lockOptions, this));
    }

    @Override
    public void refresh(String entityName, Object object, Map refreshedAlready) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        fireRefresh(refreshedAlready, new RefreshEvent(entityName, object, this));
    }

    private void fireRefresh(final RefreshEvent event) {
        try {
            if (!getSessionFactory().getSessionFactoryOptions().isAllowRefreshDetachedEntity()) {
                if (event.getEntityName() != null) {
                    if (!contains(event.getEntityName(), event.getObject())) {
                        throw new IllegalArgumentException("Entity not managed");
                    }
                } else {
                    if (!contains(event.getObject())) {
                        throw new IllegalArgumentException("Entity not managed");
                    }
                }
            }
            pulseTransactionCoordinator();
            fastSessionServices.eventListenerGroup_REFRESH.fireEventOnEachListener(event,
                    RefreshEventListener::onRefresh);
        } catch (RuntimeException e) {
            if (!getSessionFactory().getSessionFactoryOptions().isJpaBootstrap()) {
                if (e instanceof HibernateException) {
                    throw e;
                }
            }
            //including HibernateException
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    private void fireRefresh(final Map refreshedAlready, final RefreshEvent event) {
        try {
            pulseTransactionCoordinator();
            fastSessionServices.eventListenerGroup_REFRESH.fireEventOnEachListener(event, refreshedAlready,
                    RefreshEventListener::onRefresh);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        } finally {
            delayedAfterCompletion();
        }
    }

    // replicate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public void replicate(Object obj, ReplicationMode replicationMode) throws HibernateException {
        fireReplicate(new ReplicateEvent(obj, replicationMode, this));
    }

    @Override
    public void replicate(String entityName, Object obj, ReplicationMode replicationMode)
            throws HibernateException {
        fireReplicate(new ReplicateEvent(entityName, obj, replicationMode, this));
    }

    private void fireReplicate(final ReplicateEvent event) {
        checkOpen();
        pulseTransactionCoordinator();
        fastSessionServices.eventListenerGroup_REPLICATE.fireEventOnEachListener(event,
                ReplicateEventListener::onReplicate);
        delayedAfterCompletion();
    }

    // evict() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /**
     * remove any hard references to the entity that are held by the infrastructure
     * (references held by application or other persistent instances are okay)
     */
    @Override
    public void evict(Object object) throws HibernateException {
        checkOpen();
        pulseTransactionCoordinator();
        final EvictEvent event = new EvictEvent(object, this);
        fastSessionServices.eventListenerGroup_EVICT.fireEventOnEachListener(event, EvictEventListener::onEvict);
        delayedAfterCompletion();
    }

    /**
     * detect in-memory changes, determine if the changes are to tables
     * named in the query and, if so, complete execution the flush
     */
    protected boolean autoFlushIfRequired(Set querySpaces) throws HibernateException {
        checkOpen();
        if (!isTransactionInProgress()) {
            // do not auto-flush while outside a transaction
            return false;
        }
        AutoFlushEvent event = new AutoFlushEvent(querySpaces, this);
        fastSessionServices.eventListenerGroup_AUTO_FLUSH.fireEventOnEachListener(event,
                AutoFlushEventListener::onAutoFlush);
        return event.isFlushRequired();
    }

    @Override
    public boolean isDirty() throws HibernateException {
        checkOpen();
        pulseTransactionCoordinator();
        log.debug("Checking session dirtiness");
        if (actionQueue.areInsertionsOrDeletionsQueued()) {
            log.debug("Session dirty (scheduled updates and insertions)");
            return true;
        }
        DirtyCheckEvent event = new DirtyCheckEvent(this);
        fastSessionServices.eventListenerGroup_DIRTY_CHECK.fireEventOnEachListener(event,
                DirtyCheckEventListener::onDirtyCheck);
        delayedAfterCompletion();
        return event.isDirty();
    }

    @Override
    public void flush() throws HibernateException {
        checkOpen();
        doFlush();
    }

    private void doFlush() {
        checkTransactionNeededForUpdateOperation();
        pulseTransactionCoordinator();

        try {
            if (persistenceContext.getCascadeLevel() > 0) {
                throw new HibernateException("Flush during cascade is dangerous");
            }

            FlushEvent event = new FlushEvent(this);
            fastSessionServices.eventListenerGroup_FLUSH.fireEventOnEachListener(event,
                    FlushEventListener::onFlush);
            delayedAfterCompletion();
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public void setFlushMode(FlushModeType flushModeType) {
        checkOpen();
        setHibernateFlushMode(FlushModeTypeHelper.getFlushMode(flushModeType));
    }

    @Override
    public void forceFlush(EntityEntry entityEntry) throws HibernateException {
        if (log.isDebugEnabled()) {
            log.debugf("Flushing to force deletion of re-saved object: %s",
                    MessageHelper.infoString(entityEntry.getPersister(), entityEntry.getId(), getFactory()));
        }

        if (persistenceContext.getCascadeLevel() > 0) {
            throw new ObjectDeletedException(
                    "deleted object would be re-saved by cascade (remove deleted object from associations)",
                    entityEntry.getId(), entityEntry.getPersister().getEntityName());
        }
        checkOpenOrWaitingForAutoClose();
        doFlush();
    }

    @Override
    public List list(String query, QueryParameters queryParameters) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        queryParameters.validateParameters();

        HQLQueryPlan plan = queryParameters.getQueryPlan();
        if (plan == null) {
            plan = getQueryPlan(query, false);
        }

        autoFlushIfRequired(plan.getQuerySpaces());

        final List results;
        boolean success = false;

        dontFlushFromFind++; //stops flush being called multiple times if this method is recursively called
        try {
            results = plan.performList(queryParameters, this);
            success = true;
        } finally {
            dontFlushFromFind--;
            afterOperation(success);
            delayedAfterCompletion();
        }
        return results;
    }

    @Override
    public int executeUpdate(String query, QueryParameters queryParameters) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        queryParameters.validateParameters();
        HQLQueryPlan plan = getQueryPlan(query, false);
        autoFlushIfRequired(plan.getQuerySpaces());

        verifyImmutableEntityUpdate(plan);

        boolean success = false;
        int result = 0;
        try {
            result = plan.performExecuteUpdate(queryParameters, this);
            success = true;
        } finally {
            afterOperation(success);
            delayedAfterCompletion();
        }
        return result;
    }

    private void verifyImmutableEntityUpdate(HQLQueryPlan plan) {
        if (plan.isUpdate()) {
            List<String> primaryFromClauseTables = new ArrayList<>();
            for (QueryTranslator queryTranslator : plan.getTranslators()) {
                primaryFromClauseTables.addAll(queryTranslator.getPrimaryFromClauseTables());
            }
            for (EntityPersister entityPersister : getSessionFactory().getMetamodel().entityPersisters().values()) {
                if (!entityPersister.isMutable()) {
                    List<Serializable> entityQuerySpaces = new ArrayList<>(
                            Arrays.asList(entityPersister.getQuerySpaces()));
                    boolean matching = false;
                    for (Serializable entityQuerySpace : entityQuerySpaces) {
                        if (primaryFromClauseTables.contains(entityQuerySpace)) {
                            matching = true;
                            break;
                        }
                    }

                    if (matching) {
                        ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode = getSessionFactory()
                                .getSessionFactoryOptions().getImmutableEntityUpdateQueryHandlingMode();

                        String querySpaces = Arrays.toString(entityQuerySpaces.toArray());

                        switch (immutableEntityUpdateQueryHandlingMode) {
                        case WARNING:
                            log.immutableEntityUpdateQuery(plan.getSourceQuery(), querySpaces);
                            break;
                        case EXCEPTION:
                            throw new HibernateException("The query: [" + plan.getSourceQuery()
                                    + "] attempts to update an immutable entity: " + querySpaces);
                        default:
                            throw new UnsupportedOperationException(
                                    "The " + immutableEntityUpdateQueryHandlingMode + " is not supported!");

                        }
                    }
                }
            }
        }
    }

    @Override
    public int executeNativeUpdate(NativeSQLQuerySpecification nativeQuerySpecification,
            QueryParameters queryParameters) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        queryParameters.validateParameters();
        NativeSQLQueryPlan plan = getNativeQueryPlan(nativeQuerySpecification);

        autoFlushIfRequired(plan.getCustomQuery().getQuerySpaces());

        boolean success = false;
        int result = 0;
        try {
            result = plan.performExecuteUpdate(queryParameters, this);
            success = true;
        } finally {
            afterOperation(success);
            delayedAfterCompletion();
        }
        return result;
    }

    @Override
    public Iterator iterate(String query, QueryParameters queryParameters) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        queryParameters.validateParameters();

        HQLQueryPlan plan = queryParameters.getQueryPlan();
        if (plan == null) {
            plan = getQueryPlan(query, true);
        }

        autoFlushIfRequired(plan.getQuerySpaces());

        dontFlushFromFind++; //stops flush being called multiple times if this method is recursively called
        try {
            return plan.performIterate(queryParameters, this);
        } finally {
            delayedAfterCompletion();
            dontFlushFromFind--;
        }
    }

    @Override
    public ScrollableResultsImplementor scroll(String query, QueryParameters queryParameters)
            throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();

        HQLQueryPlan plan = queryParameters.getQueryPlan();
        if (plan == null) {
            plan = getQueryPlan(query, false);
        }

        autoFlushIfRequired(plan.getQuerySpaces());

        dontFlushFromFind++;
        try {
            return plan.performScroll(queryParameters, this);
        } finally {
            delayedAfterCompletion();
            dontFlushFromFind--;
        }
    }

    @Override
    public org.hibernate.query.Query createFilter(Object collection, String queryString) {
        checkOpen();
        pulseTransactionCoordinator();
        CollectionFilterImpl filter = new CollectionFilterImpl(queryString, collection, this,
                getFilterQueryPlan(collection, queryString, null, false).getParameterMetadata());
        filter.setComment(queryString);
        delayedAfterCompletion();
        return filter;
    }

    @Override
    public Object instantiate(String entityName, Serializable id) throws HibernateException {
        return instantiate(getFactory().getMetamodel().entityPersister(entityName), id);
    }

    /**
     * give the interceptor an opportunity to override the default instantiation
     */
    @Override
    public Object instantiate(EntityPersister persister, Serializable id) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        Object result = getInterceptor().instantiate(persister.getEntityName(),
                persister.getEntityMetamodel().getEntityMode(), id);
        if (result == null) {
            result = persister.instantiate(id, this);
        }
        delayedAfterCompletion();
        return result;
    }

    @Override
    public EntityPersister getEntityPersister(final String entityName, final Object object) {
        checkOpenOrWaitingForAutoClose();
        if (entityName == null) {
            return getFactory().getMetamodel().entityPersister(guessEntityName(object));
        } else {
            // try block is a hack around fact that currently tuplizers are not
            // given the opportunity to resolve a subclass entity name.  this
            // allows the (we assume custom) interceptor the ability to
            // influence this decision if we were not able to based on the
            // given entityName
            try {
                return getFactory().getMetamodel().entityPersister(entityName).getSubclassEntityPersister(object,
                        getFactory());
            } catch (HibernateException e) {
                try {
                    return getEntityPersister(null, object);
                } catch (HibernateException e2) {
                    throw e;
                }
            }
        }
    }

    // not for internal use:
    @Override
    public Serializable getIdentifier(Object object) throws HibernateException {
        checkOpen();
        checkTransactionSynchStatus();
        if (object instanceof HibernateProxy) {
            LazyInitializer li = ((HibernateProxy) object).getHibernateLazyInitializer();
            if (li.getSession() != this) {
                throw new TransientObjectException("The proxy was not associated with this session");
            }
            return li.getIdentifier();
        } else {
            EntityEntry entry = persistenceContext.getEntry(object);
            if (entry == null) {
                throw new TransientObjectException("The instance was not associated with this session");
            }
            return entry.getId();
        }
    }

    /**
     * Get the id value for an object that is actually associated with the session. This
     * is a bit stricter than getEntityIdentifierIfNotUnsaved().
     */
    @Override
    public Serializable getContextEntityIdentifier(Object object) {
        checkOpenOrWaitingForAutoClose();
        if (object instanceof HibernateProxy) {
            return getProxyIdentifier(object);
        } else {
            EntityEntry entry = persistenceContext.getEntry(object);
            return entry != null ? entry.getId() : null;
        }
    }

    private Serializable getProxyIdentifier(Object proxy) {
        return ((HibernateProxy) proxy).getHibernateLazyInitializer().getIdentifier();
    }

    private FilterQueryPlan getFilterQueryPlan(Object collection, String filter, QueryParameters parameters,
            boolean shallow) throws HibernateException {
        if (collection == null) {
            throw new NullPointerException("null collection passed to filter");
        }

        CollectionEntry entry = persistenceContext.getCollectionEntryOrNull(collection);
        final CollectionPersister roleBeforeFlush = (entry == null) ? null : entry.getLoadedPersister();

        FilterQueryPlan plan = null;
        final Map<String, Filter> enabledFilters = getLoadQueryInfluencers().getEnabledFilters();
        final SessionFactoryImplementor factory = getFactory();
        if (roleBeforeFlush == null) {
            // if it was previously unreferenced, we need to flush in order to
            // get its state into the database in order to execute query
            flush();
            entry = persistenceContext.getCollectionEntryOrNull(collection);
            CollectionPersister roleAfterFlush = (entry == null) ? null : entry.getLoadedPersister();
            if (roleAfterFlush == null) {
                throw new QueryException("The collection was unreferenced");
            }
            plan = factory.getQueryPlanCache().getFilterQueryPlan(filter, roleAfterFlush.getRole(), shallow,
                    enabledFilters);
        } else {
            // otherwise, we only need to flush if there are in-memory changes
            // to the queried tables
            plan = factory.getQueryPlanCache().getFilterQueryPlan(filter, roleBeforeFlush.getRole(), shallow,
                    enabledFilters);
            if (autoFlushIfRequired(plan.getQuerySpaces())) {
                // might need to run a different filter entirely after the flush
                // because the collection role may have changed
                entry = persistenceContext.getCollectionEntryOrNull(collection);
                CollectionPersister roleAfterFlush = (entry == null) ? null : entry.getLoadedPersister();
                if (roleBeforeFlush != roleAfterFlush) {
                    if (roleAfterFlush == null) {
                        throw new QueryException("The collection was dereferenced");
                    }
                    plan = factory.getQueryPlanCache().getFilterQueryPlan(filter, roleAfterFlush.getRole(), shallow,
                            enabledFilters);
                }
            }
        }

        if (parameters != null) {
            parameters.getNamedParameters().put(CollectionFilterKeyParameterSpecification.PARAM_KEY,
                    new TypedValue(entry.getLoadedPersister().getKeyType(), entry.getLoadedKey()));
        }

        return plan;
    }

    @Override
    public List listFilter(Object collection, String filter, QueryParameters queryParameters) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        FilterQueryPlan plan = getFilterQueryPlan(collection, filter, queryParameters, false);
        List results = Collections.EMPTY_LIST;

        boolean success = false;
        dontFlushFromFind++; //stops flush being called multiple times if this method is recursively called
        try {
            results = plan.performList(queryParameters, this);
            success = true;
        } finally {
            dontFlushFromFind--;
            afterOperation(success);
            delayedAfterCompletion();
        }
        return results;
    }

    @Override
    public Iterator iterateFilter(Object collection, String filter, QueryParameters queryParameters) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        FilterQueryPlan plan = getFilterQueryPlan(collection, filter, queryParameters, true);
        Iterator itr = plan.performIterate(queryParameters, this);
        delayedAfterCompletion();
        return itr;
    }

    @Override
    public Criteria createCriteria(Class persistentClass, String alias) {
        DeprecationLogger.DEPRECATION_LOGGER.deprecatedLegacyCriteria();
        checkOpen();
        checkTransactionSynchStatus();
        return new CriteriaImpl(persistentClass.getName(), alias, this);
    }

    @Override
    public Criteria createCriteria(String entityName, String alias) {
        DeprecationLogger.DEPRECATION_LOGGER.deprecatedLegacyCriteria();
        checkOpen();
        checkTransactionSynchStatus();
        return new CriteriaImpl(entityName, alias, this);
    }

    @Override
    public Criteria createCriteria(Class persistentClass) {
        DeprecationLogger.DEPRECATION_LOGGER.deprecatedLegacyCriteria();
        checkOpen();
        checkTransactionSynchStatus();
        return new CriteriaImpl(persistentClass.getName(), this);
    }

    @Override
    public Criteria createCriteria(String entityName) {
        DeprecationLogger.DEPRECATION_LOGGER.deprecatedLegacyCriteria();
        checkOpen();
        checkTransactionSynchStatus();
        return new CriteriaImpl(entityName, this);
    }

    @Override
    public ScrollableResultsImplementor scroll(Criteria criteria, ScrollMode scrollMode) {
        // TODO: Is this guaranteed to always be CriteriaImpl?
        CriteriaImpl criteriaImpl = (CriteriaImpl) criteria;

        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();

        String entityName = criteriaImpl.getEntityOrClassName();
        CriteriaLoader loader = new CriteriaLoader(getOuterJoinLoadable(entityName), getFactory(), criteriaImpl,
                entityName, getLoadQueryInfluencers());
        autoFlushIfRequired(loader.getQuerySpaces());
        dontFlushFromFind++;
        try {
            return loader.scroll(this, scrollMode);
        } finally {
            delayedAfterCompletion();
            dontFlushFromFind--;
        }
    }

    @Override
    public List list(Criteria criteria) throws HibernateException {
        // TODO: Is this guaranteed to always be CriteriaImpl?
        CriteriaImpl criteriaImpl = (CriteriaImpl) criteria;
        if (criteriaImpl.getMaxResults() != null && criteriaImpl.getMaxResults() == 0) {
            return Collections.EMPTY_LIST;
        }

        final NaturalIdLoadAccess naturalIdLoadAccess = this.tryNaturalIdLoadAccess(criteriaImpl);
        if (naturalIdLoadAccess != null) {
            // EARLY EXIT!
            return Arrays.asList(naturalIdLoadAccess.load());
        }

        checkOpenOrWaitingForAutoClose();
        //      checkTransactionSynchStatus();

        String[] implementors = getFactory().getMetamodel().getImplementors(criteriaImpl.getEntityOrClassName());
        int size = implementors.length;

        CriteriaLoader[] loaders = new CriteriaLoader[size];
        Set spaces = new HashSet();
        for (int i = 0; i < size; i++) {

            loaders[i] = new CriteriaLoader(getOuterJoinLoadable(implementors[i]), getFactory(), criteriaImpl,
                    implementors[i], getLoadQueryInfluencers());

            spaces.addAll(loaders[i].getQuerySpaces());

        }

        autoFlushIfRequired(spaces);

        List results = Collections.EMPTY_LIST;
        dontFlushFromFind++;
        boolean success = false;
        try {
            for (int i = 0; i < size; i++) {
                final List currentResults = loaders[i].list(this);
                currentResults.addAll(results);
                results = currentResults;
            }
            success = true;
        } finally {
            dontFlushFromFind--;
            afterOperation(success);
            delayedAfterCompletion();
        }

        return results;
    }

    /**
     * Checks to see if the CriteriaImpl is a naturalId lookup that can be done via
     * NaturalIdLoadAccess
     *
     * @param criteria The criteria to check as a complete natural identifier lookup.
     *
     * @return A fully configured NaturalIdLoadAccess or null, if null is returned the standard CriteriaImpl execution
     * should be performed
     */
    private NaturalIdLoadAccess tryNaturalIdLoadAccess(CriteriaImpl criteria) {
        // See if the criteria lookup is by naturalId
        if (!criteria.isLookupByNaturalKey()) {
            return null;
        }

        final String entityName = criteria.getEntityOrClassName();
        final EntityPersister entityPersister = getFactory().getMetamodel().entityPersister(entityName);

        // Verify the entity actually has a natural id, needed for legacy support as NaturalIdentifier criteria
        // queries did no natural id validation
        if (!entityPersister.hasNaturalIdentifier()) {
            return null;
        }

        // Since isLookupByNaturalKey is true there can be only one CriterionEntry and getCriterion() will
        // return an instanceof NaturalIdentifier
        final CriterionEntry criterionEntry = criteria.iterateExpressionEntries().next();
        final NaturalIdentifier naturalIdentifier = (NaturalIdentifier) criterionEntry.getCriterion();

        final Map<String, Object> naturalIdValues = naturalIdentifier.getNaturalIdValues();
        final int[] naturalIdentifierProperties = entityPersister.getNaturalIdentifierProperties();

        // Verify the NaturalIdentifier criterion includes all naturalId properties, first check that the property counts match
        if (naturalIdentifierProperties.length != naturalIdValues.size()) {
            return null;
        }

        final String[] propertyNames = entityPersister.getPropertyNames();
        final NaturalIdLoadAccess naturalIdLoader = this.byNaturalId(entityName);

        // Build NaturalIdLoadAccess and in the process verify all naturalId properties were specified
        for (int naturalIdentifierProperty : naturalIdentifierProperties) {
            final String naturalIdProperty = propertyNames[naturalIdentifierProperty];
            final Object naturalIdValue = naturalIdValues.get(naturalIdProperty);

            if (naturalIdValue == null) {
                // A NaturalId property is missing from the critera query, can't use NaturalIdLoadAccess
                return null;
            }

            naturalIdLoader.using(naturalIdProperty, naturalIdValue);
        }

        // Criteria query contains a valid naturalId, use the new API
        log.warn("Session.byNaturalId(" + entityName
                + ") should be used for naturalId queries instead of Restrictions.naturalId() from a Criteria");

        return naturalIdLoader;
    }

    private OuterJoinLoadable getOuterJoinLoadable(String entityName) throws MappingException {
        EntityPersister persister = getFactory().getMetamodel().entityPersister(entityName);
        if (!(persister instanceof OuterJoinLoadable)) {
            throw new MappingException("class persister is not OuterJoinLoadable: " + entityName);
        }
        return (OuterJoinLoadable) persister;
    }

    @Override
    public boolean contains(Object object) {
        checkOpen();
        pulseTransactionCoordinator();

        if (object == null) {
            return false;
        }

        try {
            if (object instanceof HibernateProxy) {
                //do not use proxiesByKey, since not all
                //proxies that point to this session's
                //instances are in that collection!
                LazyInitializer li = ((HibernateProxy) object).getHibernateLazyInitializer();
                if (li.isUninitialized()) {
                    //if it is an uninitialized proxy, pointing
                    //with this session, then when it is accessed,
                    //the underlying instance will be "contained"
                    return li.getSession() == this;
                } else {
                    //if it is initialized, see if the underlying
                    //instance is contained, since we need to
                    //account for the fact that it might have been
                    //evicted
                    object = li.getImplementation();
                }
            }

            // A session is considered to contain an entity only if the entity has
            // an entry in the session's persistence context and the entry reports
            // that the entity has not been removed
            EntityEntry entry = persistenceContext.getEntry(object);
            delayedAfterCompletion();

            if (entry == null) {
                if (!HibernateProxy.class.isInstance(object) && persistenceContext.getEntry(object) == null) {
                    // check if it is even an entity -> if not throw an exception (per JPA)
                    try {
                        final String entityName = getEntityNameResolver().resolveEntityName(object);
                        if (entityName == null) {
                            throw new IllegalArgumentException("Could not resolve entity-name [" + object + "]");
                        }
                        getSessionFactory().getMetamodel().entityPersister(entityName);
                    } catch (HibernateException e) {
                        throw new IllegalArgumentException("Not an entity [" + object.getClass() + "]", e);
                    }
                }
                return false;
            } else {
                return entry.getStatus() != Status.DELETED && entry.getStatus() != Status.GONE;
            }
        } catch (MappingException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public boolean contains(String entityName, Object object) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();

        if (object == null) {
            return false;
        }

        try {
            if (!HibernateProxy.class.isInstance(object) && persistenceContext.getEntry(object) == null) {
                // check if it is an entity -> if not throw an exception (per JPA)
                try {
                    getSessionFactory().getMetamodel().entityPersister(entityName);
                } catch (HibernateException e) {
                    throw new IllegalArgumentException("Not an entity [" + entityName + "] : " + object);
                }
            }

            if (object instanceof HibernateProxy) {
                //do not use proxiesByKey, since not all
                //proxies that point to this session's
                //instances are in that collection!
                LazyInitializer li = ((HibernateProxy) object).getHibernateLazyInitializer();
                if (li.isUninitialized()) {
                    //if it is an uninitialized proxy, pointing
                    //with this session, then when it is accessed,
                    //the underlying instance will be "contained"
                    return li.getSession() == this;
                } else {
                    //if it is initialized, see if the underlying
                    //instance is contained, since we need to
                    //account for the fact that it might have been
                    //evicted
                    object = li.getImplementation();
                }
            }
            // A session is considered to contain an entity only if the entity has
            // an entry in the session's persistence context and the entry reports
            // that the entity has not been removed
            EntityEntry entry = persistenceContext.getEntry(object);
            delayedAfterCompletion();
            return entry != null && entry.getStatus() != Status.DELETED && entry.getStatus() != Status.GONE;
        } catch (MappingException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public ProcedureCall createStoredProcedureCall(String procedureName) {
        checkOpen();
        //      checkTransactionSynchStatus();
        return super.createStoredProcedureCall(procedureName);
    }

    @Override
    public ProcedureCall createStoredProcedureCall(String procedureName, String... resultSetMappings) {
        checkOpen();
        //      checkTransactionSynchStatus();
        return super.createStoredProcedureCall(procedureName, resultSetMappings);
    }

    @Override
    public ProcedureCall createStoredProcedureCall(String procedureName, Class... resultClasses) {
        checkOpen();
        //      checkTransactionSynchStatus();
        return super.createStoredProcedureCall(procedureName, resultClasses);
    }

    @Override
    public ScrollableResultsImplementor scrollCustomQuery(CustomQuery customQuery,
            QueryParameters queryParameters) {
        checkOpenOrWaitingForAutoClose();
        //      checkTransactionSynchStatus();

        if (log.isTraceEnabled()) {
            log.tracev("Scroll SQL query: {0}", customQuery.getSQL());
        }

        CustomLoader loader = getFactory().getQueryPlanCache().getNativeQueryInterpreter()
                .createCustomLoader(customQuery, getFactory());

        autoFlushIfRequired(loader.getQuerySpaces());

        dontFlushFromFind++; //stops flush being called multiple times if this method is recursively called
        try {
            return loader.scroll(queryParameters, this);
        } finally {
            delayedAfterCompletion();
            dontFlushFromFind--;
        }
    }

    // basically just an adapted copy of find(CriteriaImpl)
    @Override
    public List listCustomQuery(CustomQuery customQuery, QueryParameters queryParameters) {
        checkOpenOrWaitingForAutoClose();
        //      checkTransactionSynchStatus();

        if (log.isTraceEnabled()) {
            log.tracev("SQL query: {0}", customQuery.getSQL());
        }

        CustomLoader loader = getFactory().getQueryPlanCache().getNativeQueryInterpreter()
                .createCustomLoader(customQuery, getFactory());

        autoFlushIfRequired(loader.getQuerySpaces());

        dontFlushFromFind++;
        boolean success = false;
        try {
            List results = loader.list(this, queryParameters);
            success = true;
            return results;
        } finally {
            dontFlushFromFind--;
            delayedAfterCompletion();
            afterOperation(success);
        }
    }

    @Override
    public SessionFactoryImplementor getSessionFactory() {
        //      checkTransactionSynchStatus();
        return getFactory();
    }

    @Override
    public void initializeCollection(PersistentCollection collection, boolean writing) {
        checkOpenOrWaitingForAutoClose();
        pulseTransactionCoordinator();
        InitializeCollectionEvent event = new InitializeCollectionEvent(collection, this);
        fastSessionServices.eventListenerGroup_INIT_COLLECTION.fireEventOnEachListener(event,
                InitializeCollectionEventListener::onInitializeCollection);
        delayedAfterCompletion();
    }

    @Override
    public String bestGuessEntityName(Object object) {
        if (object instanceof HibernateProxy) {
            LazyInitializer initializer = ((HibernateProxy) object).getHibernateLazyInitializer();
            // it is possible for this method to be called during flush processing,
            // so make certain that we do not accidentally initialize an uninitialized proxy
            if (initializer.isUninitialized()) {
                return initializer.getEntityName();
            }
            object = initializer.getImplementation();
        }
        EntityEntry entry = persistenceContext.getEntry(object);
        if (entry == null) {
            return guessEntityName(object);
        } else {
            return entry.getPersister().getEntityName();
        }
    }

    @Override
    public String getEntityName(Object object) {
        checkOpen();
        //      checkTransactionSynchStatus();
        if (object instanceof HibernateProxy) {
            if (!persistenceContext.containsProxy(object)) {
                throw new TransientObjectException("proxy was not associated with the session");
            }
            object = ((HibernateProxy) object).getHibernateLazyInitializer().getImplementation();
        }

        EntityEntry entry = persistenceContext.getEntry(object);
        if (entry == null) {
            throwTransientObjectException(object);
        }
        return entry.getPersister().getEntityName();
    }

    private void throwTransientObjectException(Object object) throws HibernateException {
        throw new TransientObjectException(
                "object references an unsaved transient instance - save the transient instance before flushing: "
                        + guessEntityName(object));
    }

    @Override
    public String guessEntityName(Object object) throws HibernateException {
        checkOpenOrWaitingForAutoClose();
        return getEntityNameResolver().resolveEntityName(object);
    }

    @Override
    public void cancelQuery() throws HibernateException {
        checkOpen();
        getJdbcCoordinator().cancelLastQuery();
    }

    @Override
    public int getDontFlushFromFind() {
        return dontFlushFromFind;
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(500).append("SessionImpl(").append(System.identityHashCode(this));
        if (!isClosed()) {
            if (log.isTraceEnabled()) {
                buf.append(persistenceContext).append(";").append(actionQueue);
            } else {
                buf.append("<open>");
            }
        } else {
            buf.append("<closed>");
        }
        return buf.append(')').toString();
    }

    @Override
    public ActionQueue getActionQueue() {
        checkOpenOrWaitingForAutoClose();
        //      checkTransactionSynchStatus();
        return actionQueue;
    }

    @Override
    public PersistenceContext getPersistenceContext() {
        checkOpenOrWaitingForAutoClose();
        //      checkTransactionSynchStatus();
        return persistenceContext;
    }

    @Override
    public PersistenceContext getPersistenceContextInternal() {
        return persistenceContext;
    }

    @Override
    public SessionStatistics getStatistics() {
        pulseTransactionCoordinator();
        return new SessionStatisticsImpl(this);
    }

    @Override
    public boolean isEventSource() {
        pulseTransactionCoordinator();
        return true;
    }

    @Override
    public boolean isDefaultReadOnly() {
        return persistenceContext.isDefaultReadOnly();
    }

    @Override
    public void setDefaultReadOnly(boolean defaultReadOnly) {
        persistenceContext.setDefaultReadOnly(defaultReadOnly);
    }

    @Override
    public boolean isReadOnly(Object entityOrProxy) {
        checkOpen();
        //      checkTransactionSynchStatus();
        return persistenceContext.isReadOnly(entityOrProxy);
    }

    @Override
    public void setReadOnly(Object entity, boolean readOnly) {
        checkOpen();
        //      checkTransactionSynchStatus();
        persistenceContext.setReadOnly(entity, readOnly);
    }

    @Override
    public void doWork(final Work work) throws HibernateException {
        WorkExecutorVisitable<Void> realWork = new WorkExecutorVisitable<Void>() {
            @Override
            public Void accept(WorkExecutor<Void> workExecutor, Connection connection) throws SQLException {
                workExecutor.executeWork(work, connection);
                return null;
            }
        };
        doWork(realWork);
    }

    @Override
    public <T> T doReturningWork(final ReturningWork<T> work) throws HibernateException {
        WorkExecutorVisitable<T> realWork = new WorkExecutorVisitable<T>() {
            @Override
            public T accept(WorkExecutor<T> workExecutor, Connection connection) throws SQLException {
                return workExecutor.executeReturningWork(work, connection);
            }
        };
        return doWork(realWork);
    }

    private <T> T doWork(WorkExecutorVisitable<T> work) throws HibernateException {
        return getJdbcCoordinator().coordinateWork(work);
    }

    @Override
    public void afterScrollOperation() {
        // nothing to do in a stateful session
    }

    @Override
    public LoadQueryInfluencers getLoadQueryInfluencers() {
        return loadQueryInfluencers;
    }

    // filter support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public Filter getEnabledFilter(String filterName) {
        pulseTransactionCoordinator();
        return loadQueryInfluencers.getEnabledFilter(filterName);
    }

    @Override
    public Filter enableFilter(String filterName) {
        checkOpen();
        pulseTransactionCoordinator();
        return loadQueryInfluencers.enableFilter(filterName);
    }

    @Override
    public void disableFilter(String filterName) {
        checkOpen();
        pulseTransactionCoordinator();
        loadQueryInfluencers.disableFilter(filterName);
    }

    // fetch profile support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    @Override
    public boolean isFetchProfileEnabled(String name) throws UnknownProfileException {
        return loadQueryInfluencers.isFetchProfileEnabled(name);
    }

    @Override
    public void enableFetchProfile(String name) throws UnknownProfileException {
        loadQueryInfluencers.enableFetchProfile(name);
    }

    @Override
    public void disableFetchProfile(String name) throws UnknownProfileException {
        loadQueryInfluencers.disableFetchProfile(name);
    }

    @Override
    public TypeHelper getTypeHelper() {
        return getSessionFactory().getTypeHelper();
    }

    @Override
    public LobHelper getLobHelper() {
        if (lobHelper == null) {
            lobHelper = new LobHelperImpl(this);
        }
        return lobHelper;
    }

    private transient LobHelperImpl lobHelper;

    private Transaction getTransactionIfAccessible() {
        // We do not want an exception to be thrown if the transaction
        // is not accessible. If the transaction is not accessible,
        // then return null.
        return fastSessionServices.isJtaTransactionAccessible ? accessTransaction() : null;
    }

    @Override
    public void beforeTransactionCompletion() {
        log.trace("SessionImpl#beforeTransactionCompletion()");
        flushBeforeTransactionCompletion();
        actionQueue.beforeTransactionCompletion();
        try {
            getInterceptor().beforeTransactionCompletion(getTransactionIfAccessible());
        } catch (Throwable t) {
            log.exceptionInBeforeTransactionCompletionInterceptor(t);
        }
        super.beforeTransactionCompletion();
    }

    @Override
    public void afterTransactionCompletion(boolean successful, boolean delayed) {
        if (log.isTraceEnabled()) {
            log.tracef("SessionImpl#afterTransactionCompletion(successful=%s, delayed=%s)", successful, delayed);
        }

        if (!isClosed() || waitingForAutoClose) {
            if (autoClear || !successful) {
                internalClear();
            }
        }

        persistenceContext.afterTransactionCompletion();
        actionQueue.afterTransactionCompletion(successful);

        getEventListenerManager().transactionCompletion(successful);

        final StatisticsImplementor statistics = getFactory().getStatistics();
        if (statistics.isStatisticsEnabled()) {
            statistics.endTransaction(successful);
        }

        try {
            getInterceptor().afterTransactionCompletion(getTransactionIfAccessible());
        } catch (Throwable t) {
            log.exceptionInAfterTransactionCompletionInterceptor(t);
        }

        if (!delayed) {
            if (shouldAutoClose() && (!isClosed() || waitingForAutoClose)) {
                managedClose();
            }
        }

        super.afterTransactionCompletion(successful, delayed);
    }

    private static class LobHelperImpl implements LobHelper {
        private final SessionImpl session;

        private LobHelperImpl(SessionImpl session) {
            this.session = session;
        }

        @Override
        public Blob createBlob(byte[] bytes) {
            return lobCreator().createBlob(bytes);
        }

        private LobCreator lobCreator() {
            // Always use NonContextualLobCreator.  If ContextualLobCreator is
            // used both here and in WrapperOptions,
            return NonContextualLobCreator.INSTANCE;
        }

        @Override
        public Blob createBlob(InputStream stream, long length) {
            return lobCreator().createBlob(stream, length);
        }

        @Override
        public Clob createClob(String string) {
            return lobCreator().createClob(string);
        }

        @Override
        public Clob createClob(Reader reader, long length) {
            return lobCreator().createClob(reader, length);
        }

        @Override
        public NClob createNClob(String string) {
            return lobCreator().createNClob(string);
        }

        @Override
        public NClob createNClob(Reader reader, long length) {
            return lobCreator().createNClob(reader, length);
        }
    }

    private static class SharedSessionBuilderImpl<T extends SharedSessionBuilder>
            extends SessionFactoryImpl.SessionBuilderImpl<T>
            implements SharedSessionBuilder<T>, SharedSessionCreationOptions {
        private final SessionImpl session;
        private boolean shareTransactionContext;

        private SharedSessionBuilderImpl(SessionImpl session) {
            super((SessionFactoryImpl) session.getFactory());
            this.session = session;
            super.tenantIdentifier(session.getTenantIdentifier());
        }

        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // SharedSessionBuilder

        @Override
        public T tenantIdentifier(String tenantIdentifier) {
            // todo : is this always true?  Or just in the case of sharing JDBC resources?
            throw new SessionException("Cannot redefine tenant identifier on child session");
        }

        @Override
        public T interceptor() {
            return interceptor(session.getInterceptor());
        }

        @Override
        @SuppressWarnings("unchecked")
        public T connection() {
            this.shareTransactionContext = true;
            return (T) this;
        }

        @Override
        public T connectionReleaseMode() {
            return connectionReleaseMode(session.getJdbcCoordinator().getLogicalConnection()
                    .getConnectionHandlingMode().getReleaseMode());
        }

        @Override
        public T connectionHandlingMode() {
            return connectionHandlingMode(
                    session.getJdbcCoordinator().getLogicalConnection().getConnectionHandlingMode());
        }

        @Override
        public T autoJoinTransactions() {
            return autoJoinTransactions(session.isAutoCloseSessionEnabled());
        }

        @Override
        public T flushMode() {
            return flushMode(session.getHibernateFlushMode());
        }

        @Override
        public T autoClose() {
            return autoClose(session.autoClose);
        }

        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // SharedSessionCreationOptions

        @Override
        public boolean isTransactionCoordinatorShared() {
            return shareTransactionContext;
        }

        @Override
        public TransactionCoordinator getTransactionCoordinator() {
            return shareTransactionContext ? session.getTransactionCoordinator() : null;
        }

        @Override
        public JdbcCoordinator getJdbcCoordinator() {
            return shareTransactionContext ? session.getJdbcCoordinator() : null;
        }

        @Override
        public TransactionImplementor getTransaction() {
            return shareTransactionContext ? session.getCurrentTransaction() : null;
        }

        @Override
        public ActionQueue.TransactionCompletionProcesses getTransactionCompletionProcesses() {
            return shareTransactionContext ? session.getActionQueue().getTransactionCompletionProcesses() : null;
        }

        @Override
        public boolean isQueryParametersValidationEnabled() {
            return session.isQueryParametersValidationEnabled();
        }
    }

    private class LockRequestImpl implements LockRequest {
        private final LockOptions lockOptions;

        private LockRequestImpl(LockOptions lo) {
            lockOptions = new LockOptions();
            LockOptions.copy(lo, lockOptions);
        }

        @Override
        public LockMode getLockMode() {
            return lockOptions.getLockMode();
        }

        @Override
        public LockRequest setLockMode(LockMode lockMode) {
            lockOptions.setLockMode(lockMode);
            return this;
        }

        @Override
        public int getTimeOut() {
            return lockOptions.getTimeOut();
        }

        @Override
        public LockRequest setTimeOut(int timeout) {
            lockOptions.setTimeOut(timeout);
            return this;
        }

        @Override
        public boolean getScope() {
            return lockOptions.getScope();
        }

        @Override
        public LockRequest setScope(boolean scope) {
            lockOptions.setScope(scope);
            return this;
        }

        @Override
        public void lock(String entityName, Object object) throws HibernateException {
            fireLock(entityName, object, lockOptions);
        }

        @Override
        public void lock(Object object) throws HibernateException {
            fireLock(object, lockOptions);
        }
    }

    @Override
    protected void addSharedSessionTransactionObserver(TransactionCoordinator transactionCoordinator) {
        this.transactionObserver = new TransactionObserver() {
            @Override
            public void afterBegin() {
            }

            @Override
            public void beforeCompletion() {
                if (isOpen() && getHibernateFlushMode() != FlushMode.MANUAL) {
                    managedFlush();
                }
                actionQueue.beforeTransactionCompletion();
                try {
                    getInterceptor().beforeTransactionCompletion(getTransactionIfAccessible());
                } catch (Throwable t) {
                    log.exceptionInBeforeTransactionCompletionInterceptor(t);
                }
            }

            @Override
            public void afterCompletion(boolean successful, boolean delayed) {
                afterTransactionCompletion(successful, delayed);
                if (!isClosed() && autoClose) {
                    managedClose();
                }
            }
        };
        transactionCoordinator.addObserver(transactionObserver);
    }

    @Override
    protected void removeSharedSessionTransactionObserver(TransactionCoordinator transactionCoordinator) {
        super.removeSharedSessionTransactionObserver(transactionCoordinator);
        transactionCoordinator.removeObserver(transactionObserver);
    }

    private class IdentifierLoadAccessImpl<T> implements IdentifierLoadAccess<T> {
        private final EntityPersister entityPersister;

        private LockOptions lockOptions;
        private CacheMode cacheMode;
        private RootGraphImplementor<T> rootGraph;
        private GraphSemantic graphSemantic;

        private IdentifierLoadAccessImpl(EntityPersister entityPersister) {
            this.entityPersister = entityPersister;
        }

        private IdentifierLoadAccessImpl(String entityName) {
            this(locateEntityPersister(entityName));
        }

        private IdentifierLoadAccessImpl(Class<T> entityClass) {
            this(locateEntityPersister(entityClass));
        }

        @Override
        public final IdentifierLoadAccessImpl<T> with(LockOptions lockOptions) {
            this.lockOptions = lockOptions;
            return this;
        }

        @Override
        public IdentifierLoadAccess<T> with(CacheMode cacheMode) {
            this.cacheMode = cacheMode;
            return this;
        }

        @Override
        public IdentifierLoadAccess<T> with(RootGraph<T> graph, GraphSemantic semantic) {
            this.rootGraph = (RootGraphImplementor<T>) graph;
            this.graphSemantic = semantic;
            return this;
        }

        @Override
        public final T getReference(Serializable id) {
            return perform(() -> doGetReference(id));
        }

        protected T perform(Supplier<T> executor) {
            CacheMode sessionCacheMode = getCacheMode();
            boolean cacheModeChanged = false;
            if (cacheMode != null) {
                // naive check for now...
                // todo : account for "conceptually equal"
                if (cacheMode != sessionCacheMode) {
                    setCacheMode(cacheMode);
                    cacheModeChanged = true;
                }
            }

            try {
                if (graphSemantic != null) {
                    if (rootGraph == null) {
                        throw new IllegalArgumentException(
                                "Graph semantic specified, but no RootGraph was supplied");
                    }
                    loadQueryInfluencers.getEffectiveEntityGraph().applyGraph(rootGraph, graphSemantic);
                }

                try {
                    return executor.get();
                } finally {
                    if (graphSemantic != null) {
                        loadQueryInfluencers.getEffectiveEntityGraph().clear();
                    }
                }
            } finally {
                if (cacheModeChanged) {
                    // change it back
                    setCacheMode(sessionCacheMode);
                }
            }
        }

        @SuppressWarnings("unchecked")
        protected T doGetReference(Serializable id) {
            if (this.lockOptions != null) {
                LoadEvent event = new LoadEvent(id, entityPersister.getEntityName(), lockOptions, SessionImpl.this);
                fireLoad(event, LoadEventListener.LOAD);
                return (T) event.getResult();
            }

            LoadEvent event = new LoadEvent(id, entityPersister.getEntityName(), false, SessionImpl.this);
            boolean success = false;
            try {
                fireLoad(event, LoadEventListener.LOAD);
                if (event.getResult() == null) {
                    getFactory().getEntityNotFoundDelegate().handleEntityNotFound(entityPersister.getEntityName(),
                            id);
                }
                success = true;
                return (T) event.getResult();
            } finally {
                afterOperation(success);
            }
        }

        @Override
        public final T load(Serializable id) {
            return perform(() -> doLoad(id));
        }

        @Override
        public Optional<T> loadOptional(Serializable id) {
            return Optional.ofNullable(perform(() -> doLoad(id)));
        }

        @SuppressWarnings("unchecked")
        protected final T doLoad(Serializable id) {
            if (this.lockOptions != null) {
                LoadEvent event = new LoadEvent(id, entityPersister.getEntityName(), lockOptions, SessionImpl.this);
                fireLoad(event, LoadEventListener.GET);
                return (T) event.getResult();
            }

            LoadEvent event = new LoadEvent(id, entityPersister.getEntityName(), false, SessionImpl.this);
            boolean success = false;
            try {
                fireLoad(event, LoadEventListener.GET);
                success = true;
            } catch (ObjectNotFoundException e) {
                // if session cache contains proxy for non-existing object
            } finally {
                afterOperation(success);
            }
            return (T) event.getResult();
        }
    }

    private class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>, MultiLoadOptions {
        private final EntityPersister entityPersister;

        private LockOptions lockOptions;
        private CacheMode cacheMode;

        private RootGraphImplementor<T> rootGraph;
        private GraphSemantic graphSemantic;

        private Integer batchSize;
        private boolean sessionCheckingEnabled;
        private boolean returnOfDeletedEntitiesEnabled;
        private boolean orderedReturnEnabled = true;

        public MultiIdentifierLoadAccessImpl(EntityPersister entityPersister) {
            this.entityPersister = entityPersister;
        }

        @Override
        public LockOptions getLockOptions() {
            return lockOptions;
        }

        @Override
        public final MultiIdentifierLoadAccess<T> with(LockOptions lockOptions) {
            this.lockOptions = lockOptions;
            return this;
        }

        @Override
        public MultiIdentifierLoadAccess<T> with(CacheMode cacheMode) {
            this.cacheMode = cacheMode;
            return this;
        }

        @Override
        public MultiIdentifierLoadAccess<T> with(RootGraph<T> graph, GraphSemantic semantic) {
            this.rootGraph = (RootGraphImplementor<T>) graph;
            this.graphSemantic = semantic;
            return this;
        }

        @Override
        public Integer getBatchSize() {
            return batchSize;
        }

        @Override
        public MultiIdentifierLoadAccess<T> withBatchSize(int batchSize) {
            if (batchSize < 1) {
                this.batchSize = null;
            } else {
                this.batchSize = batchSize;
            }
            return this;
        }

        @Override
        public boolean isSessionCheckingEnabled() {
            return sessionCheckingEnabled;
        }

        @Override
        public boolean isSecondLevelCacheCheckingEnabled() {
            return cacheMode == CacheMode.NORMAL || cacheMode == CacheMode.GET;
        }

        @Override
        public MultiIdentifierLoadAccess<T> enableSessionCheck(boolean enabled) {
            this.sessionCheckingEnabled = enabled;
            return this;
        }

        @Override
        public boolean isReturnOfDeletedEntitiesEnabled() {
            return returnOfDeletedEntitiesEnabled;
        }

        @Override
        public MultiIdentifierLoadAccess<T> enableReturnOfDeletedEntities(boolean enabled) {
            this.returnOfDeletedEntitiesEnabled = enabled;
            return this;
        }

        @Override
        public boolean isOrderReturnEnabled() {
            return orderedReturnEnabled;
        }

        @Override
        public MultiIdentifierLoadAccess<T> enableOrderedReturn(boolean enabled) {
            this.orderedReturnEnabled = enabled;
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public <K extends Serializable> List<T> multiLoad(K... ids) {
            return perform(() -> entityPersister.multiLoad(ids, SessionImpl.this, this));
        }

        public List<T> perform(Supplier<List<T>> executor) {
            CacheMode sessionCacheMode = getCacheMode();
            boolean cacheModeChanged = false;
            if (cacheMode != null) {
                // naive check for now...
                // todo : account for "conceptually equal"
                if (cacheMode != sessionCacheMode) {
                    setCacheMode(cacheMode);
                    cacheModeChanged = true;
                }
            }

            try {
                if (graphSemantic != null) {
                    if (rootGraph == null) {
                        throw new IllegalArgumentException(
                                "Graph semantic specified, but no RootGraph was supplied");
                    }
                    loadQueryInfluencers.getEffectiveEntityGraph().applyGraph(rootGraph, graphSemantic);
                }

                try {
                    return executor.get();
                } finally {
                    if (graphSemantic != null) {
                        loadQueryInfluencers.getEffectiveEntityGraph().clear();
                    }
                }
            } finally {
                if (cacheModeChanged) {
                    // change it back
                    setCacheMode(sessionCacheMode);
                }
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public <K extends Serializable> List<T> multiLoad(List<K> ids) {
            return perform(() -> entityPersister.multiLoad(ids.toArray(new Serializable[ids.size()]),
                    SessionImpl.this, this));
        }
    }

    private EntityPersister locateEntityPersister(Class entityClass) {
        return getFactory().getMetamodel().locateEntityPersister(entityClass);
    }

    private EntityPersister locateEntityPersister(String entityName) {
        return getFactory().getMetamodel().locateEntityPersister(entityName);
    }

    private abstract class BaseNaturalIdLoadAccessImpl<T> {
        private final EntityPersister entityPersister;
        private LockOptions lockOptions;
        private boolean synchronizationEnabled = true;

        private BaseNaturalIdLoadAccessImpl(EntityPersister entityPersister) {
            this.entityPersister = entityPersister;

            if (!entityPersister.hasNaturalIdentifier()) {
                throw new HibernateException(
                        String.format("Entity [%s] did not define a natural id", entityPersister.getEntityName()));
            }
        }

        public BaseNaturalIdLoadAccessImpl<T> with(LockOptions lockOptions) {
            this.lockOptions = lockOptions;
            return this;
        }

        protected void synchronizationEnabled(boolean synchronizationEnabled) {
            this.synchronizationEnabled = synchronizationEnabled;
        }

        protected final Serializable resolveNaturalId(Map<String, Object> naturalIdParameters) {
            performAnyNeededCrossReferenceSynchronizations();

            final ResolveNaturalIdEvent event = new ResolveNaturalIdEvent(naturalIdParameters, entityPersister,
                    SessionImpl.this);
            fireResolveNaturalId(event);

            if (event.getEntityId() == PersistenceContext.NaturalIdHelper.INVALID_NATURAL_ID_REFERENCE) {
                return null;
            } else {
                return event.getEntityId();
            }
        }

        protected void performAnyNeededCrossReferenceSynchronizations() {
            if (!synchronizationEnabled) {
                // synchronization (this process) was disabled
                return;
            }
            if (entityPersister.getEntityMetamodel().hasImmutableNaturalId()) {
                // only mutable natural-ids need this processing
                return;
            }
            if (!isTransactionInProgress()) {
                // not in a transaction so skip synchronization
                return;
            }

            final PersistenceContext persistenceContext = getPersistenceContextInternal();
            final boolean debugEnabled = log.isDebugEnabled();
            for (Serializable pk : persistenceContext.getNaturalIdHelper()
                    .getCachedPkResolutions(entityPersister)) {
                final EntityKey entityKey = generateEntityKey(pk, entityPersister);
                final Object entity = persistenceContext.getEntity(entityKey);
                final EntityEntry entry = persistenceContext.getEntry(entity);

                if (entry == null) {
                    if (debugEnabled) {
                        log.debug(
                                "Cached natural-id/pk resolution linked to null EntityEntry in persistence context : "
                                        + MessageHelper.infoString(entityPersister, pk, getFactory()));
                    }
                    continue;
                }

                if (!entry.requiresDirtyCheck(entity)) {
                    continue;
                }

                // MANAGED is the only status we care about here...
                if (entry.getStatus() != Status.MANAGED) {
                    continue;
                }

                persistenceContext.getNaturalIdHelper().handleSynchronization(entityPersister, pk, entity);
            }
        }

        protected final IdentifierLoadAccess getIdentifierLoadAccess() {
            final IdentifierLoadAccessImpl identifierLoadAccess = new IdentifierLoadAccessImpl(entityPersister);
            if (this.lockOptions != null) {
                identifierLoadAccess.with(lockOptions);
            }
            return identifierLoadAccess;
        }

        protected EntityPersister entityPersister() {
            return entityPersister;
        }
    }

    private class NaturalIdLoadAccessImpl<T> extends BaseNaturalIdLoadAccessImpl<T>
            implements NaturalIdLoadAccess<T> {
        private final Map<String, Object> naturalIdParameters = new LinkedHashMap<String, Object>();

        private NaturalIdLoadAccessImpl(EntityPersister entityPersister) {
            super(entityPersister);
        }

        private NaturalIdLoadAccessImpl(String entityName) {
            this(locateEntityPersister(entityName));
        }

        private NaturalIdLoadAccessImpl(Class entityClass) {
            this(locateEntityPersister(entityClass));
        }

        @Override
        public NaturalIdLoadAccessImpl<T> with(LockOptions lockOptions) {
            return (NaturalIdLoadAccessImpl<T>) super.with(lockOptions);
        }

        @Override
        public NaturalIdLoadAccess<T> using(String attributeName, Object value) {
            naturalIdParameters.put(attributeName, value);
            return this;
        }

        @Override
        public NaturalIdLoadAccessImpl<T> setSynchronizationEnabled(boolean synchronizationEnabled) {
            super.synchronizationEnabled(synchronizationEnabled);
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public final T getReference() {
            final Serializable entityId = resolveNaturalId(this.naturalIdParameters);
            if (entityId == null) {
                return null;
            }
            return (T) this.getIdentifierLoadAccess().getReference(entityId);
        }

        @Override
        @SuppressWarnings("unchecked")
        public final T load() {
            final Serializable entityId = resolveNaturalId(this.naturalIdParameters);
            if (entityId == null) {
                return null;
            }
            try {
                return (T) this.getIdentifierLoadAccess().load(entityId);
            } catch (EntityNotFoundException | ObjectNotFoundException enf) {
                // OK
            }
            return null;
        }

        @Override
        public Optional<T> loadOptional() {
            return Optional.ofNullable(load());
        }
    }

    private class SimpleNaturalIdLoadAccessImpl<T> extends BaseNaturalIdLoadAccessImpl<T>
            implements SimpleNaturalIdLoadAccess<T> {
        private final String naturalIdAttributeName;

        private SimpleNaturalIdLoadAccessImpl(EntityPersister entityPersister) {
            super(entityPersister);

            if (entityPersister.getNaturalIdentifierProperties().length != 1) {
                throw new HibernateException(String.format("Entity [%s] did not define a simple natural id",
                        entityPersister.getEntityName()));
            }

            final int naturalIdAttributePosition = entityPersister.getNaturalIdentifierProperties()[0];
            this.naturalIdAttributeName = entityPersister.getPropertyNames()[naturalIdAttributePosition];
        }

        private SimpleNaturalIdLoadAccessImpl(String entityName) {
            this(locateEntityPersister(entityName));
        }

        private SimpleNaturalIdLoadAccessImpl(Class entityClass) {
            this(locateEntityPersister(entityClass));
        }

        @Override
        public final SimpleNaturalIdLoadAccessImpl<T> with(LockOptions lockOptions) {
            return (SimpleNaturalIdLoadAccessImpl<T>) super.with(lockOptions);
        }

        private Map<String, Object> getNaturalIdParameters(Object naturalIdValue) {
            return Collections.singletonMap(naturalIdAttributeName, naturalIdValue);
        }

        @Override
        public SimpleNaturalIdLoadAccessImpl<T> setSynchronizationEnabled(boolean synchronizationEnabled) {
            super.synchronizationEnabled(synchronizationEnabled);
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T getReference(Object naturalIdValue) {
            final Serializable entityId = resolveNaturalId(getNaturalIdParameters(naturalIdValue));
            if (entityId == null) {
                return null;
            }
            return (T) this.getIdentifierLoadAccess().getReference(entityId);
        }

        @Override
        @SuppressWarnings("unchecked")
        public T load(Object naturalIdValue) {
            final Serializable entityId = resolveNaturalId(getNaturalIdParameters(naturalIdValue));
            if (entityId == null) {
                return null;
            }
            try {
                return (T) this.getIdentifierLoadAccess().load(entityId);
            } catch (EntityNotFoundException | ObjectNotFoundException e) {
                // OK
            }
            return null;
        }

        @Override
        public Optional<T> loadOptional(Serializable naturalIdValue) {
            return Optional.ofNullable(load(naturalIdValue));
        }
    }

    @Override
    public void startTransactionBoundary() {
        checkOpenOrWaitingForAutoClose();
        super.startTransactionBoundary();
    }

    @Override
    public void afterTransactionBegin() {
        checkOpenOrWaitingForAutoClose();
        getInterceptor().afterTransactionBegin(getTransactionIfAccessible());
    }

    @Override
    public void flushBeforeTransactionCompletion() {
        final boolean doFlush = isTransactionFlushable() && getHibernateFlushMode() != FlushMode.MANUAL;

        try {
            if (doFlush) {
                managedFlush();
            }
        } catch (RuntimeException re) {
            throw ExceptionMapperStandardImpl.INSTANCE.mapManagedFlushFailure("error during managed flush", re,
                    this);
        }
    }

    private boolean isTransactionFlushable() {
        if (getCurrentTransaction() == null) {
            // assume it is flushable - CMT, auto-commit, etc
            return true;
        }
        final TransactionStatus status = getCurrentTransaction().getStatus();
        return status == TransactionStatus.ACTIVE || status == TransactionStatus.COMMITTING;
    }

    @Override
    public boolean isFlushBeforeCompletionEnabled() {
        return getHibernateFlushMode() != FlushMode.MANUAL;
    }

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // HibernateEntityManager impl

    @Override
    public SessionImplementor getSession() {
        return this;
    }

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // HibernateEntityManagerImplementor impl

    @Override
    public LockOptions getLockRequest(LockModeType lockModeType, Map<String, Object> properties) {
        LockOptions lockOptions = new LockOptions();
        if (this.lockOptions != null) { //otherwise the default LockOptions constructor is the same as DEFAULT_LOCK_OPTIONS
            LockOptions.copy(this.lockOptions, lockOptions);
        }
        lockOptions.setLockMode(LockModeTypeHelper.getLockMode(lockModeType));
        if (properties != null) {
            LockOptionsHelper.applyPropertiesToLockOptions(properties, () -> lockOptions);
        }
        return lockOptions;
    }

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // EntityManager impl

    @Override
    public void remove(Object entity) {
        checkOpen();

        try {
            delete(entity);
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            //including HibernateException
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey) {
        return find(entityClass, primaryKey, null, null);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties) {
        return find(entityClass, primaryKey, null, properties);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockModeType) {
        return find(entityClass, primaryKey, lockModeType, null);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockModeType,
            Map<String, Object> properties) {
        checkOpen();

        LockOptions lockOptions = null;

        try {
            getLoadQueryInfluencers().getEffectiveEntityGraph().applyConfiguredGraph(properties);

            final IdentifierLoadAccess<T> loadAccess = byId(entityClass);
            loadAccess.with(determineAppropriateLocalCacheMode(properties));

            if (lockModeType != null) {
                if (!LockModeType.NONE.equals(lockModeType)) {
                    checkTransactionNeededForUpdateOperation();
                }
                lockOptions = buildLockOptions(lockModeType, properties);
                loadAccess.with(lockOptions);
            }

            return loadAccess.load((Serializable) primaryKey);
        } catch (EntityNotFoundException ignored) {
            // DefaultLoadEventListener.returnNarrowedProxy may throw ENFE (see HHH-7861 for details),
            // which find() should not throw.  Find() should return null if the entity was not found.
            if (log.isDebugEnabled()) {
                String entityName = entityClass != null ? entityClass.getName() : null;
                String identifierValue = primaryKey != null ? primaryKey.toString() : null;
                log.ignoringEntityNotFound(entityName, identifierValue);
            }
            return null;
        } catch (ObjectDeletedException e) {
            //the spec is silent about people doing remove() find() on the same PC
            return null;
        } catch (ObjectNotFoundException e) {
            //should not happen on the entity itself with get
            throw new IllegalArgumentException(e.getMessage(), e);
        } catch (MappingException | TypeMismatchException | ClassCastException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (JDBCException e) {
            if (accessTransaction().isActive() && accessTransaction().getRollbackOnly()) {
                // Assume this is the similar to the WildFly / IronJacamar "feature" described under HHH-12472.
                // Just log the exception and return null.
                if (log.isDebugEnabled()) {
                    log.debug("JDBCException was thrown for a transaction marked for rollback; "
                            + "this is probably due to an operation failing fast due to the "
                            + "transaction marked for rollback.", e);
                }
                return null;
            } else {
                throw getExceptionConverter().convert(e, lockOptions);
            }
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e, lockOptions);
        } finally {
            getLoadQueryInfluencers().getEffectiveEntityGraph().clear();
        }
    }

    private CacheMode determineAppropriateLocalCacheMode(Map<String, Object> localProperties) {
        CacheRetrieveMode retrieveMode = null;
        CacheStoreMode storeMode = null;
        if (localProperties != null) {
            retrieveMode = determineCacheRetrieveMode(localProperties);
            storeMode = determineCacheStoreMode(localProperties);
        }
        if (retrieveMode == null) {
            // use the EM setting
            retrieveMode = fastSessionServices.getCacheRetrieveMode(this.properties);
        }
        if (storeMode == null) {
            // use the EM setting
            storeMode = fastSessionServices.getCacheStoreMode(this.properties);
        }
        return CacheModeHelper.interpretCacheMode(storeMode, retrieveMode);
    }

    private static CacheRetrieveMode determineCacheRetrieveMode(Map<String, Object> settings) {
        return (CacheRetrieveMode) settings.get(JPA_SHARED_CACHE_RETRIEVE_MODE);
    }

    private static CacheStoreMode determineCacheStoreMode(Map<String, Object> settings) {
        return (CacheStoreMode) settings.get(JPA_SHARED_CACHE_STORE_MODE);
    }

    private void checkTransactionNeededForUpdateOperation() {
        checkTransactionNeededForUpdateOperation("no transaction is in progress");
    }

    @Override
    public <T> T getReference(Class<T> entityClass, Object primaryKey) {
        checkOpen();

        try {
            return byId(entityClass).getReference((Serializable) primaryKey);
        } catch (MappingException | TypeMismatchException | ClassCastException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public void lock(Object entity, LockModeType lockModeType) {
        lock(entity, lockModeType, null);
    }

    @Override
    public void lock(Object entity, LockModeType lockModeType, Map<String, Object> properties) {
        checkOpen();
        checkTransactionNeededForUpdateOperation();

        if (!contains(entity)) {
            throw new IllegalArgumentException("entity not in the persistence context");
        }

        final LockOptions lockOptions = buildLockOptions(lockModeType, properties);
        try {
            buildLockRequest(lockOptions).lock(entity);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e, lockOptions);
        }
    }

    @Override
    public void refresh(Object entity, Map<String, Object> properties) {
        refresh(entity, null, properties);
    }

    @Override
    public void refresh(Object entity, LockModeType lockModeType) {
        refresh(entity, lockModeType, null);
    }

    @Override
    public void refresh(Object entity, LockModeType lockModeType, Map<String, Object> properties) {
        checkOpen();

        final CacheMode previousCacheMode = getCacheMode();
        final CacheMode refreshCacheMode = determineAppropriateLocalCacheMode(properties);

        LockOptions lockOptions = null;
        try {
            setCacheMode(refreshCacheMode);

            if (!contains(entity)) {
                throw getExceptionConverter().convert(new IllegalArgumentException("Entity not managed"));
            }

            if (lockModeType != null) {
                if (!LockModeType.NONE.equals(lockModeType)) {
                    checkTransactionNeededForUpdateOperation();
                }

                lockOptions = buildLockOptions(lockModeType, properties);
                refresh(entity, lockOptions);
            } else {
                refresh(entity);
            }
        } catch (MappingException e) {
            throw getExceptionConverter().convert(new IllegalArgumentException(e.getMessage(), e));
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e, lockOptions);
        } finally {
            setCacheMode(previousCacheMode);
        }
    }

    @Override
    public void detach(Object entity) {
        checkOpen();
        try {
            evict(entity);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public LockModeType getLockMode(Object entity) {
        checkOpen();

        if (!isTransactionInProgress()) {
            throw new TransactionRequiredException(
                    "Call to EntityManager#getLockMode should occur within transaction according to spec");
        }

        if (!contains(entity)) {
            throw getExceptionConverter()
                    .convert(new IllegalArgumentException("entity not in the persistence context"));
        }

        return LockModeTypeHelper.getLockModeType(getCurrentLockMode(entity));

    }

    @Override
    public void setProperty(String propertyName, Object value) {
        checkOpen();

        if (!(value instanceof Serializable)) {
            log.warnf("Property '" + propertyName + "' is not serializable, value won't be set.");
            return;
        }

        if (propertyName == null) {
            log.warnf("Property having key null is illegal; value won't be set.");
            return;
        }

        //Store property for future reference:

        if (properties == null) {
            properties = computeCurrentSessionProperties();
        }
        properties.put(propertyName, value);

        //now actually update settings, if it's any of these which have a direct impact on this Session state:

        if (AvailableSettings.FLUSH_MODE.equals(propertyName)) {
            setHibernateFlushMode(ConfigurationHelper.getFlushMode(value, FlushMode.AUTO));
        } else if (JPA_LOCK_SCOPE.equals(propertyName) || JPA_LOCK_TIMEOUT.equals(propertyName)) {
            LockOptionsHelper.applyPropertiesToLockOptions(properties, this::getLockOptionsForWrite);
        } else if (JPA_SHARED_CACHE_RETRIEVE_MODE.equals(propertyName)
                || JPA_SHARED_CACHE_STORE_MODE.equals(propertyName)) {
            getSession().setCacheMode(CacheModeHelper.interpretCacheMode(determineCacheStoreMode(properties),
                    determineCacheRetrieveMode(properties)));
        }
    }

    private Map<String, Object> computeCurrentSessionProperties() {
        final HashMap<String, Object> map = new HashMap<>(fastSessionServices.defaultSessionProperties);
        //The FLUSH_MODE is always set at Session creation time, so it needs special treatment to not eagerly initialize this Map:
        map.put(AvailableSettings.FLUSH_MODE, getHibernateFlushMode().name());
        return map;
    }

    @Override
    public Map<String, Object> getProperties() {
        if (properties == null) {
            properties = computeCurrentSessionProperties();
        }
        return Collections.unmodifiableMap(properties);
    }

    @Override
    protected void initQueryFromNamedDefinition(Query query, NamedQueryDefinition namedQueryDefinition) {
        super.initQueryFromNamedDefinition(query, namedQueryDefinition);

        if (namedQueryDefinition.getLockOptions() != null) {
            if (namedQueryDefinition.getLockOptions().getLockMode() != null) {
                query.setLockMode(
                        LockModeTypeHelper.getLockModeType(namedQueryDefinition.getLockOptions().getLockMode()));
            }
        }
    }

    @Override
    public StoredProcedureQuery createNamedStoredProcedureQuery(String name) {
        checkOpen();
        try {
            final ProcedureCallMemento memento = getFactory().getNamedQueryRepository()
                    .getNamedProcedureCallMemento(name);
            if (memento == null) {
                throw new IllegalArgumentException(
                        "No @NamedStoredProcedureQuery was found with that name : " + name);
            }
            return memento.makeProcedureCall(this);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public StoredProcedureQuery createStoredProcedureQuery(String procedureName) {
        try {
            return createStoredProcedureCall(procedureName);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses) {
        try {
            return createStoredProcedureCall(procedureName, resultClasses);
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) {
        checkOpen();
        try {
            try {
                return createStoredProcedureCall(procedureName, resultSetMappings);
            } catch (UnknownSqlResultSetMappingException unknownResultSetMapping) {
                throw new IllegalArgumentException(unknownResultSetMapping.getMessage(), unknownResultSetMapping);
            }
        } catch (RuntimeException e) {
            throw getExceptionConverter().convert(e);
        }
    }

    @Override
    public void joinTransaction() {
        checkOpen();
        joinTransaction(true);
    }

    private void joinTransaction(boolean explicitRequest) {
        if (!getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta()) {
            if (explicitRequest) {
                log.callingJoinTransactionOnNonJtaEntityManager();
            }
            return;
        }

        try {
            getTransactionCoordinator().explicitJoin();
        } catch (TransactionRequiredForJoinException e) {
            throw new TransactionRequiredException(e.getMessage());
        } catch (HibernateException he) {
            throw getExceptionConverter().convert(he);
        }
    }

    @Override
    public boolean isJoinedToTransaction() {
        checkOpen();
        return getTransactionCoordinator().isJoined();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> clazz) {
        checkOpen();

        if (Session.class.isAssignableFrom(clazz)) {
            return (T) this;
        }
        if (SessionImplementor.class.isAssignableFrom(clazz)) {
            return (T) this;
        }
        if (SharedSessionContractImplementor.class.isAssignableFrom(clazz)) {
            return (T) this;
        }
        if (EntityManager.class.isAssignableFrom(clazz)) {
            return (T) this;
        }

        throw new PersistenceException("Hibernate cannot unwrap " + clazz);
    }

    @Override
    public Object getDelegate() {
        checkOpen();
        return this;
    }

    @Override
    public SessionFactoryImplementor getEntityManagerFactory() {
        checkOpen();
        return getFactory();
    }

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

    @Override
    public MetamodelImplementor getMetamodel() {
        checkOpen();
        return getFactory().getMetamodel();
    }

    @Override
    public <T> RootGraphImplementor<T> createEntityGraph(Class<T> rootType) {
        checkOpen();
        return new RootGraphImpl<T>(null, getMetamodel().entity(rootType), getEntityManagerFactory());
    }

    @Override
    public RootGraphImplementor<?> createEntityGraph(String graphName) {
        checkOpen();
        final RootGraphImplementor named = getEntityManagerFactory().findEntityGraphByName(graphName);
        if (named != null) {
            return named.makeRootGraph(graphName, true);
        }
        return named;
    }

    @Override
    @SuppressWarnings("unchecked")
    public RootGraphImplementor<?> getEntityGraph(String graphName) {
        checkOpen();
        final RootGraphImplementor named = getEntityManagerFactory().findEntityGraphByName(graphName);
        if (named == null) {
            throw new IllegalArgumentException("Could not locate EntityGraph with given name : " + graphName);
        }
        return named;
    }

    @Override
    public List getEntityGraphs(Class entityClass) {
        checkOpen();
        return getEntityManagerFactory().findEntityGraphsByType(entityClass);
    }

    /**
     * Used by JDK serialization...
     *
     * @param oos The output stream to which we are being written...
     *
     * @throws IOException Indicates a general IO stream exception
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        if (log.isTraceEnabled()) {
            log.tracef("Serializing Session [%s]", getSessionIdentifier());
        }

        oos.defaultWriteObject();

        persistenceContext.serialize(oos);
        actionQueue.serialize(oos);

        oos.writeObject(loadQueryInfluencers);
    }

    /**
     * Used by JDK serialization...
     *
     * @param ois The input stream from which we are being read...
     *
     * @throws IOException Indicates a general IO stream exception
     * @throws ClassNotFoundException Indicates a class resolution issue
     */
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException, SQLException {
        if (log.isTraceEnabled()) {
            log.tracef("Deserializing Session [%s]", getSessionIdentifier());
        }

        ois.defaultReadObject();

        persistenceContext = StatefulPersistenceContext.deserialize(ois, this);
        actionQueue = ActionQueue.deserialize(ois, this);

        loadQueryInfluencers = (LoadQueryInfluencers) ois.readObject();

        // LoadQueryInfluencers.getEnabledFilters() tries to validate each enabled
        // filter, which will fail when called before FilterImpl.afterDeserialize( factory );
        // Instead lookup the filter by name and then call FilterImpl.afterDeserialize( factory ).
        for (String filterName : loadQueryInfluencers.getEnabledFilterNames()) {
            ((FilterImpl) loadQueryInfluencers.getEnabledFilter(filterName)).afterDeserialize(getFactory());
        }
    }
}