borinquenkid commented on code in PR #15568:
URL: https://github.com/apache/grails-core/pull/15568#discussion_r3440428452


##########
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:
   Follow-up — here is the actual delta between `GrailsHibernateTemplate` H5 
and H7. All removals are intentional:
   
   | Removed from H7 | Reason |
   |---|---|
   | `applySettings(Criteria criteria)` | `org.hibernate.Criteria` (legacy H5 
criteria API) was dropped in Hibernate 6 |
   | `prepareCriteria(Criteria criteria)` | Same — no legacy `Criteria` in H6+ |
   | `delete(Object entity)` | Renamed to `remove(Object entity)` per Jakarta 
Persistence naming (`EntityManager.remove()`) |
   | `flush(Object entity)` | Per-entity flush is no longer a first-class 
operation; dirty checking handles this |
   
   Everything else (`get`, `load`, `lock`, `loadAll`, `contains`, `evict`, 
`refresh`, `executeWithNewSession`, `execute`, `setFlushMode`, etc.) is present 
in H7. The H7 class also adds `FLUSH_ALWAYS` and `deleteAll(Collection)`.



-- 
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]

Reply via email to