jdaugherty commented on code in PR #15568: URL: https://github.com/apache/grails-core/pull/15568#discussion_r3241499851
########## grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java: ########## @@ -0,0 +1,764 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate; + +import java.io.Serializable; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import javax.sql.DataSource; + +import groovy.lang.Closure; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +import jakarta.persistence.LockModeType; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.LockMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.event.spi.EventSource; +import org.hibernate.exception.GenericJDBCException; +import org.hibernate.query.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.util.Assert; + +import org.grails.orm.hibernate.support.hibernate7.DefaultTransactionResources; +import org.grails.orm.hibernate.support.hibernate7.SessionFactoryUtils; +import org.grails.orm.hibernate.support.hibernate7.SessionHolder; +import org.grails.orm.hibernate.support.hibernate7.TransactionResources; + +@SuppressWarnings({"PMD.CloseResource", "PMD.DataflowAnomalyAnalysis", "PMD.CompareObjectsWithEquals", "PMD.EmptyIfStmt" +}) +public class GrailsHibernateTemplate implements IHibernateTemplate { + + /** + * Never flush is a good strategy for read-only units of work. + * Hibernate will not track and look + * for changes in this case, avoiding any overhead of modification detection. + * + * <p>In case of an existing Session, FLUSH_NEVER will turn the flush mode to NEVER for the scope + * of the current operation, resetting the previous flush mode afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_NEVER = 0; + /** + * Automatic flushing is the default mode for a Hibernate Session. A session will get flushed on + * transaction commit, and on certain find operations that might involve already modified + * instances, but not after each unit of work like with eager flushing. + * + * <p>In case of an existing Session, FLUSH_AUTO will participate in the existing flush mode, not + * modifying it for the current operation. This in particular means that this setting will not + * modify an existing flush mode NEVER, in contrast to FLUSH_EAGER. + * + * @see #setFlushMode + */ + public static final int FLUSH_AUTO = 1; + /** + * Eager flushing leads to immediate synchronization with the database, even if in a transaction. + * This causes inconsistencies to show up and throw a respective exception immediately, and JDBC + * access code that participates in the same transaction will see the changes as the database is + * already aware of them then. But the drawbacks are: + * + * <ul> + * <li>additional communication roundtrips with the database, instead of a single batch at + * transaction commit; + * <li>the fact that an actual database rollback is needed if the Hibernate transaction rolls + * back (due to already submitted SQL statements). + * </ul> + * + * <p>In case of an existing Session, FLUSH_EAGER will turn the flush mode to AUTO for the scope + * of the current operation and issue a flush at the end, resetting the previous flush mode + * afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_EAGER = 2; + /** + * Flushing at commit only is intended for units of work where no intermediate flushing is + * desired, not even for find operations that might involve already modified instances. + * + * <p>In case of an existing Session, FLUSH_COMMIT will turn the flush mode to COMMIT for the + * scope of the current operation, resetting the previous flush mode afterwards. The only + * exception is an existing flush mode NEVER, which will not be modified through this setting. + * + * @see #setFlushMode + */ + public static final int FLUSH_COMMIT = 3; + /** + * Flushing before every query statement is rarely necessary. It is only available for special + * needs. + * + * <p>In case of an existing Session, FLUSH_ALWAYS will turn the flush mode to ALWAYS for the + * scope of the current operation, resetting the previous flush mode afterwards. + * + * @see #setFlushMode + */ + public static final int FLUSH_ALWAYS = 4; + + private static final Logger LOG = LoggerFactory.getLogger(GrailsHibernateTemplate.class); + protected boolean exposeNativeSession = true; + protected boolean cacheQueries = false; + protected SessionFactory sessionFactory; + protected DataSource dataSource = null; + protected SQLExceptionTranslator jdbcExceptionTranslator; + protected int flushMode = FLUSH_AUTO; + private boolean osivReadOnly; + private boolean passReadOnlyToHibernate = false; + private boolean applyFlushModeOnlyToNonExistingTransactions = false; + protected TransactionResources txResources = new DefaultTransactionResources(); + + protected GrailsHibernateTemplate() { + // for testing + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "Property 'sessionFactory' is required"); + this.sessionFactory = sessionFactory; + + ConnectionProvider connectionProvider = ((SessionFactoryImplementor) sessionFactory) + .getServiceRegistry() + .getService(ConnectionProvider.class); + this.dataSource = connectionProvider != null ? connectionProvider.unwrap(DataSource.class) : null; + if (this.dataSource != null) { + if (this.dataSource instanceof TransactionAwareDataSourceProxy) { + DataSource target = ((TransactionAwareDataSourceProxy) this.dataSource).getTargetDataSource(); + if (target != null) { + this.dataSource = target; + } + } + jdbcExceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(this.dataSource); + } else { + // must be in unit test mode, setup default translator + SQLErrorCodeSQLExceptionTranslator sqlErrorCodeSQLExceptionTranslator = + new SQLErrorCodeSQLExceptionTranslator(); + sqlErrorCodeSQLExceptionTranslator.setDatabaseProductName("H2"); + jdbcExceptionTranslator = sqlErrorCodeSQLExceptionTranslator; + } + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory, HibernateDatastore datastore) { + this(sessionFactory); + if (datastore != null) { + cacheQueries = datastore.isCacheQueries(); + this.osivReadOnly = datastore.isOsivReadOnly(); + this.passReadOnlyToHibernate = datastore.isPassReadOnlyToHibernate(); + this.flushMode = hibernateFlushModeToConstant(datastore.getDefaultFlushMode()); + } + } + + public GrailsHibernateTemplate(SessionFactory sessionFactory, HibernateDatastore datastore, int defaultFlushMode) { + this(sessionFactory); + if (datastore != null) { + cacheQueries = datastore.isCacheQueries(); + this.osivReadOnly = datastore.isOsivReadOnly(); + this.passReadOnlyToHibernate = datastore.isPassReadOnlyToHibernate(); + } + this.flushMode = defaultFlushMode; + } + + /** Maps a Hibernate {@link FlushMode} to one of the {@code FLUSH_*} constants of this class. */ + static int hibernateFlushModeToConstant(FlushMode mode) { + return switch (mode) { + case MANUAL -> FLUSH_NEVER; + case COMMIT -> FLUSH_COMMIT; + case ALWAYS -> FLUSH_ALWAYS; + default -> FLUSH_AUTO; + }; + } + + @Override + public <T> T execute(Closure<T> callable) { + @SuppressWarnings("unchecked") + HibernateCallback<T> hibernateCallback = + (HibernateCallback<T>) DefaultGroovyMethods.asType(callable, HibernateCallback.class); + return execute(hibernateCallback); + } + + @SuppressWarnings("PMD.DataflowAnomalyAnalysis") + @Override + public <T> T executeWithNewSession(final Closure<T> callable) { + SessionHolder sessionHolder = (SessionHolder) txResources.getResource(sessionFactory); + SessionHolder previousHolder = sessionHolder; + ConnectionHolder previousConnectionHolder = + (ConnectionHolder) txResources.getResource(dataSource); + Session newSession = null; + boolean previousActiveSynchronization = txResources.isSynchronizationActive(); + List<TransactionSynchronization> transactionSynchronizations = + previousActiveSynchronization ? txResources.getSynchronizations() : null; + try { + // if there are any previous synchronizations active we need to clear them and restore them + // later (see finally block) + if (previousActiveSynchronization) { + txResources.clearSynchronization(); + // init a new synchronization to ensure that any opened database connections are closed by + // the synchronization + txResources.initSynchronization(); + } + + // if there are already bound holders, unbind them so they can be restored later + if (sessionHolder != null) { + txResources.unbindResource(sessionFactory); + if (previousConnectionHolder != null) { + txResources.unbindResource(dataSource); + } + } + + // create and bind a new session holder for the new session + newSession = sessionFactory.openSession(); + applyFlushMode(newSession, false); + sessionHolder = new SessionHolder(newSession); + txResources.bindResource(sessionFactory, sessionHolder); + + return callable.call(newSession); + } finally { + try { + // if an active synchronization was registered during the life time of the new session clear + // it + if (txResources.isSynchronizationActive()) { + txResources.clearSynchronization(); + } + // If there is a synchronization active then leave it to the synchronization to close the + // session + // Clear any bound sessions and connections + txResources.unbindResource(sessionFactory); + ConnectionHolder connectionHolder = + (ConnectionHolder) txResources.unbindResourceIfPossible(dataSource); + // if there is a connection holder and it holds an open connection close it + try { + if (connectionHolder != null && + !(dataSource instanceof org.grails.datastore.gorm.jdbc.MultiTenantDataSource) && + !connectionHolder.getConnection().isClosed()) { + Connection conn = connectionHolder.getConnection(); + DataSourceUtils.releaseConnection(conn, dataSource); + } + } catch (SQLException e) { + // ignore, connection closed already? + if (LOG.isDebugEnabled()) { + LOG.debug( + "Could not close opened JDBC connection. Did the application close the connection manually?: " + + e.getMessage()); + } + } + + if (newSession != null) { + SessionFactoryUtils.closeSession(newSession); + } + } finally { + // if there were previously active synchronizations then register those again + if (previousActiveSynchronization) { + txResources.initSynchronization(); + for (TransactionSynchronization transactionSynchronization : transactionSynchronizations) { + txResources.registerSynchronization(transactionSynchronization); + } + } + + // now restore any previous state + if (previousHolder != null) { + txResources.bindResource(sessionFactory, previousHolder); + if (previousConnectionHolder != null) { + txResources.bindResource(dataSource, previousConnectionHolder); + } + } + } + } + } + + @Override + public <T1> T1 executeWithExistingOrCreateNewSession(SessionFactory sessionFactory, Closure<T1> callable) { + SessionHolder sessionHolder = (SessionHolder) txResources.getResource(sessionFactory); + if (sessionHolder == null) { + return executeWithNewSession(callable); + } else { + return callable.call(sessionHolder.getSession()); + } + } + + @Override + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + @Override + public void applySettings(org.hibernate.query.Query<?> query) { + if (exposeNativeSession) { + prepareQuery(query); + } + } + + public boolean isCacheQueries() { + return cacheQueries; + } + + public void setCacheQueries(boolean cacheQueries) { + this.cacheQueries = cacheQueries; + } + + @SuppressWarnings("PMD.PreserveStackTrace") + public <T> T execute(HibernateCallback<T> action) throws DataAccessException { + return doExecute(action, false); + } + + public List<?> executeFind(HibernateCallback<?> action) throws DataAccessException { + Object result = doExecute(action, false); + if (result != null && !(result instanceof List)) { + throw new InvalidDataAccessApiUsageException( + "Result object returned from HibernateCallback isn't a List: [" + result + "]"); + } + return (List<?>) result; + } + + protected boolean shouldPassReadOnlyToHibernate() { + if ((passReadOnlyToHibernate || osivReadOnly) && + txResources.hasResource(getSessionFactory())) { + if (txResources.isActualTransactionActive()) { + return passReadOnlyToHibernate && txResources.isCurrentTransactionReadOnly(); + } else { + return osivReadOnly; + } + } else { + return false; + } + } + + public boolean isOsivReadOnly() { + return osivReadOnly; + } + + public void setOsivReadOnly(boolean osivReadOnly) { + this.osivReadOnly = osivReadOnly; + } + + /** + * Execute the action specified by the given action object within a Session. + * + * @param action callback object that specifies the Hibernate action + * @param enforceNativeSession whether to enforce exposure of the native Hibernate Session to + * callback code + * @return a result object returned by the action, or <code>null</code> + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + @SuppressWarnings("PMD.PreserveStackTrace") + protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNativeSession) throws DataAccessException { + + Assert.notNull(action, "Callback object must not be null"); + + Session session = getSession(); + boolean existingTransaction = isSessionTransactional(session); + if (existingTransaction) { + LOG.debug("Found thread-bound Session for HibernateTemplate"); + } + + FlushMode previousFlushMode = null; + try { + previousFlushMode = applyFlushMode(session, existingTransaction); + if (shouldPassReadOnlyToHibernate()) { + session.setDefaultReadOnly(true); + } + Session sessionToExpose = + (enforceNativeSession || exposeNativeSession ? session : createSessionProxy(session)); + T result = action.doInHibernate(sessionToExpose); + flushIfNecessary(session, existingTransaction); + return result; + } catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } catch (PersistenceException ex) { + if (ex.getCause() instanceof HibernateException hibernateException) { + throw SessionFactoryUtils.convertHibernateAccessException(hibernateException); + } + throw ex; + } catch (SQLException ex) { + throw Objects.requireNonNull( + jdbcExceptionTranslator.translate("Hibernate-related JDBC operation", null, ex)); + } finally { + if (existingTransaction) { + LOG.debug("Not closing pre-bound Hibernate Session after HibernateTemplate"); + if (previousFlushMode != null) { + session.setHibernateFlushMode(previousFlushMode); + } + } else { + SessionFactoryUtils.closeSession(session); + } + } + } + + protected boolean isSessionTransactional(Session session) { + SessionHolder sessionHolder = (SessionHolder) txResources.getResource(sessionFactory); + return sessionHolder != null && sessionHolder.getSession() == session; + } + + public Session getSession() { + try { + return sessionFactory.getCurrentSession(); + } catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not obtain current Hibernate Session", ex); + } + } + + /** + * Create a close-suppressing proxy for the given Hibernate Session. The proxy also prepares + * returned Query and Criteria objects. + * + * @param session the Hibernate Session to create a proxy for + * @return the Session proxy + * @see org.hibernate.Session#close() + * @see #prepareQuery + * @see #prepareCriteria + */ + protected Session createSessionProxy(Session session) { + Class<?>[] sessionIfcs; + Class<?> mainIfc = Session.class; + if (session instanceof EventSource) { + sessionIfcs = new Class[] {mainIfc, EventSource.class}; + } else if (session instanceof SessionImplementor) { + sessionIfcs = new Class[] {mainIfc, SessionImplementor.class}; + } else { + sessionIfcs = new Class[] {mainIfc}; + } + return (Session) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + sessionIfcs, + new CloseSuppressingInvocationHandler(session, this)); + } + + @Override + @Deprecated(since = "7.0", forRemoval = true) Review Comment: now that I've rebased the original changes it looks like this was added as part of your refactoring. Can you help me understand why it was marked deprecated? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
