001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.orm.jpa.vendor;
018
019import java.lang.reflect.Method;
020import java.sql.Connection;
021import java.sql.SQLException;
022import javax.persistence.EntityManager;
023import javax.persistence.PersistenceException;
024
025import org.apache.commons.logging.LogFactory;
026import org.hibernate.FlushMode;
027import org.hibernate.HibernateException;
028import org.hibernate.NonUniqueObjectException;
029import org.hibernate.NonUniqueResultException;
030import org.hibernate.ObjectDeletedException;
031import org.hibernate.OptimisticLockException;
032import org.hibernate.PersistentObjectException;
033import org.hibernate.PessimisticLockException;
034import org.hibernate.PropertyValueException;
035import org.hibernate.QueryException;
036import org.hibernate.QueryTimeoutException;
037import org.hibernate.Session;
038import org.hibernate.StaleObjectStateException;
039import org.hibernate.StaleStateException;
040import org.hibernate.TransientObjectException;
041import org.hibernate.UnresolvableObjectException;
042import org.hibernate.WrongClassException;
043import org.hibernate.exception.ConstraintViolationException;
044import org.hibernate.exception.DataException;
045import org.hibernate.exception.JDBCConnectionException;
046import org.hibernate.exception.LockAcquisitionException;
047import org.hibernate.exception.SQLGrammarException;
048
049import org.springframework.dao.CannotAcquireLockException;
050import org.springframework.dao.DataAccessException;
051import org.springframework.dao.DataAccessResourceFailureException;
052import org.springframework.dao.DataIntegrityViolationException;
053import org.springframework.dao.DuplicateKeyException;
054import org.springframework.dao.IncorrectResultSizeDataAccessException;
055import org.springframework.dao.InvalidDataAccessApiUsageException;
056import org.springframework.dao.InvalidDataAccessResourceUsageException;
057import org.springframework.dao.PessimisticLockingFailureException;
058import org.springframework.jdbc.datasource.ConnectionHandle;
059import org.springframework.jdbc.datasource.DataSourceUtils;
060import org.springframework.jdbc.support.JdbcUtils;
061import org.springframework.orm.ObjectOptimisticLockingFailureException;
062import org.springframework.orm.ObjectRetrievalFailureException;
063import org.springframework.orm.jpa.DefaultJpaDialect;
064import org.springframework.orm.jpa.EntityManagerFactoryUtils;
065import org.springframework.orm.jpa.JpaSystemException;
066import org.springframework.transaction.InvalidIsolationLevelException;
067import org.springframework.transaction.TransactionDefinition;
068import org.springframework.transaction.TransactionException;
069import org.springframework.util.Assert;
070import org.springframework.util.ClassUtils;
071import org.springframework.util.ReflectionUtils;
072
073/**
074 * {@link org.springframework.orm.jpa.JpaDialect} implementation for
075 * Hibernate EntityManager. Developed and tested against Hibernate 3.6,
076 * 4.2/4.3 as well as 5.0/5.1/5.2.
077 *
078 * @author Juergen Hoeller
079 * @author Costin Leau
080 * @since 2.0
081 * @see HibernateJpaVendorAdapter
082 * @see org.hibernate.Session#setFlushMode
083 * @see org.hibernate.Transaction#setTimeout
084 */
085@SuppressWarnings("serial")
086public class HibernateJpaDialect extends DefaultJpaDialect {
087
088        private static Class<?> optimisticLockExceptionClass;
089
090        private static Class<?> pessimisticLockExceptionClass;
091
092        private static Method getFlushMode;
093
094        static {
095                // Checking for Hibernate 4.x's Optimistic/PessimisticEntityLockException
096                ClassLoader cl = HibernateJpaDialect.class.getClassLoader();
097                try {
098                        optimisticLockExceptionClass = cl.loadClass("org.hibernate.dialect.lock.OptimisticEntityLockException");
099                }
100                catch (ClassNotFoundException ex) {
101                        // OptimisticLockException is deprecated on Hibernate 4.x; we're just using it on 3.x anyway
102                        optimisticLockExceptionClass = OptimisticLockException.class;
103                }
104                try {
105                        pessimisticLockExceptionClass = cl.loadClass("org.hibernate.dialect.lock.PessimisticEntityLockException");
106                }
107                catch (ClassNotFoundException ex) {
108                        pessimisticLockExceptionClass = null;
109                }
110
111                try {
112                        // Hibernate 5.2+ getHibernateFlushMode()
113                        getFlushMode = Session.class.getMethod("getHibernateFlushMode");
114                }
115                catch (NoSuchMethodException ex) {
116                        try {
117                                // Classic Hibernate getFlushMode() with FlushMode return type
118                                getFlushMode = Session.class.getMethod("getFlushMode");
119                        }
120                        catch (NoSuchMethodException ex2) {
121                                throw new IllegalStateException("No compatible Hibernate getFlushMode signature found", ex2);
122                        }
123                }
124                // Check that it is the Hibernate FlushMode type, not JPA's...
125                Assert.state(FlushMode.class == getFlushMode.getReturnType(), "Could not find Hibernate getFlushMode method");
126        }
127
128
129        boolean prepareConnection = (HibernateConnectionHandle.sessionConnectionMethod == null);
130
131
132        /**
133         * Set whether to prepare the underlying JDBC Connection of a transactional
134         * Hibernate Session, that is, whether to apply a transaction-specific
135         * isolation level and/or the transaction's read-only flag to the underlying
136         * JDBC Connection.
137         * <p>Default is "true" on Hibernate EntityManager 4.x (with its 'on-close'
138         * connection release mode, and "false" on Hibernate EntityManager 3.6 (due to
139         * the 'after-transaction' release mode there). <b>Note that Hibernate 4.2+ is
140         * strongly recommended in order to make isolation levels work efficiently.</b>
141         * <p>If you turn this flag off, JPA transaction management will not support
142         * per-transaction isolation levels anymore. It will not call
143         * {@code Connection.setReadOnly(true)} for read-only transactions anymore either.
144         * If this flag is turned off, no cleanup of a JDBC Connection is required after
145         * a transaction, since no Connection settings will get modified.
146         * <p><b>NOTE:</b> The default behavior in terms of read-only handling changed
147         * in Spring 4.1, propagating the read-only status to the JDBC Connection now,
148         * analogous to other Spring transaction managers. This may have the effect
149         * that you're running into read-only enforcement now where previously write
150         * access has accidentally been tolerated: Please revise your transaction
151         * declarations accordingly, removing invalid read-only markers if necessary.
152         * @since 4.1
153         * @see java.sql.Connection#setTransactionIsolation
154         * @see java.sql.Connection#setReadOnly
155         */
156        public void setPrepareConnection(boolean prepareConnection) {
157                this.prepareConnection = prepareConnection;
158        }
159
160
161        @Override
162        public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
163                        throws PersistenceException, SQLException, TransactionException {
164
165                Session session = getSession(entityManager);
166
167                if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
168                        session.getTransaction().setTimeout(definition.getTimeout());
169                }
170
171                boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
172                Integer previousIsolationLevel = null;
173                Connection preparedCon = null;
174
175                if (isolationLevelNeeded || definition.isReadOnly()) {
176                        if (this.prepareConnection) {
177                                preparedCon = HibernateConnectionHandle.doGetConnection(session);
178                                previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(preparedCon, definition);
179                        }
180                        else if (isolationLevelNeeded) {
181                                throw new InvalidIsolationLevelException(getClass().getSimpleName() +
182                                                " does not support custom isolation levels since the 'prepareConnection' flag is off. " +
183                                                "This is the case on Hibernate 3.6 by default; either switch that flag at your own risk " +
184                                                "or upgrade to Hibernate 4.x, with 4.2+ recommended.");
185                        }
186                }
187
188                // Standard JPA transaction begin call for full JPA context setup...
189                entityManager.getTransaction().begin();
190
191                // Adapt flush mode and store previous isolation level, if any.
192                FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly());
193                return new SessionTransactionData(session, previousFlushMode, preparedCon, previousIsolationLevel);
194        }
195
196        @Override
197        public Object prepareTransaction(EntityManager entityManager, boolean readOnly, String name)
198                        throws PersistenceException {
199
200                Session session = getSession(entityManager);
201                FlushMode previousFlushMode = prepareFlushMode(session, readOnly);
202                return new SessionTransactionData(session, previousFlushMode, null, null);
203        }
204
205        protected FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException {
206                FlushMode flushMode = (FlushMode) ReflectionUtils.invokeMethod(getFlushMode, session);
207                if (readOnly) {
208                        // We should suppress flushing for a read-only transaction.
209                        if (!flushMode.equals(FlushMode.MANUAL)) {
210                                session.setFlushMode(FlushMode.MANUAL);
211                                return flushMode;
212                        }
213                }
214                else {
215                        // We need AUTO or COMMIT for a non-read-only transaction.
216                        if (flushMode.lessThan(FlushMode.COMMIT)) {
217                                session.setFlushMode(FlushMode.AUTO);
218                                return flushMode;
219                        }
220                }
221                // No FlushMode change needed...
222                return null;
223        }
224
225        @Override
226        public void cleanupTransaction(Object transactionData) {
227                ((SessionTransactionData) transactionData).resetSessionState();
228        }
229
230        @Override
231        public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly)
232                        throws PersistenceException, SQLException {
233
234                Session session = getSession(entityManager);
235                return new HibernateConnectionHandle(session);
236        }
237
238        @Override
239        public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
240                if (ex instanceof HibernateException) {
241                        return convertHibernateAccessException((HibernateException) ex);
242                }
243                if (ex instanceof PersistenceException && ex.getCause() instanceof HibernateException) {
244                        return convertHibernateAccessException((HibernateException) ex.getCause());
245                }
246                return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex);
247        }
248
249        /**
250         * Convert the given HibernateException to an appropriate exception
251         * from the {@code org.springframework.dao} hierarchy.
252         * @param ex HibernateException that occurred
253         * @return the corresponding DataAccessException instance
254         */
255        protected DataAccessException convertHibernateAccessException(HibernateException ex) {
256                if (ex instanceof JDBCConnectionException) {
257                        return new DataAccessResourceFailureException(ex.getMessage(), ex);
258                }
259                if (ex instanceof SQLGrammarException) {
260                        SQLGrammarException jdbcEx = (SQLGrammarException) ex;
261                        return new InvalidDataAccessResourceUsageException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex);
262                }
263                if (ex instanceof QueryTimeoutException) {
264                        QueryTimeoutException jdbcEx = (QueryTimeoutException) ex;
265                        return new org.springframework.dao.QueryTimeoutException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex);
266                }
267                if (ex instanceof LockAcquisitionException) {
268                        LockAcquisitionException jdbcEx = (LockAcquisitionException) ex;
269                        return new CannotAcquireLockException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex);
270                }
271                if (ex instanceof PessimisticLockException) {
272                        PessimisticLockException jdbcEx = (PessimisticLockException) ex;
273                        return new PessimisticLockingFailureException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex);
274                }
275                if (ex instanceof ConstraintViolationException) {
276                        ConstraintViolationException jdbcEx = (ConstraintViolationException) ex;
277                        return new DataIntegrityViolationException(ex.getMessage()  + "; SQL [" + jdbcEx.getSQL() +
278                                        "]; constraint [" + jdbcEx.getConstraintName() + "]", ex);
279                }
280                if (ex instanceof DataException) {
281                        DataException jdbcEx = (DataException) ex;
282                        return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex);
283                }
284                // end of JDBCException subclass handling
285
286                if (ex instanceof QueryException) {
287                        return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex);
288                }
289                if (ex instanceof NonUniqueResultException) {
290                        return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex);
291                }
292                if (ex instanceof NonUniqueObjectException) {
293                        return new DuplicateKeyException(ex.getMessage(), ex);
294                }
295                if (ex instanceof PropertyValueException) {
296                        return new DataIntegrityViolationException(ex.getMessage(), ex);
297                }
298                if (ex instanceof PersistentObjectException) {
299                        return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
300                }
301                if (ex instanceof TransientObjectException) {
302                        return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
303                }
304                if (ex instanceof ObjectDeletedException) {
305                        return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
306                }
307                if (ex instanceof UnresolvableObjectException) {
308                        UnresolvableObjectException hibEx = (UnresolvableObjectException) ex;
309                        return new ObjectRetrievalFailureException(hibEx.getEntityName(), hibEx.getIdentifier(), ex.getMessage(), ex);
310                }
311                if (ex instanceof WrongClassException) {
312                        WrongClassException hibEx = (WrongClassException) ex;
313                        return new ObjectRetrievalFailureException(hibEx.getEntityName(), hibEx.getIdentifier(), ex.getMessage(), ex);
314                }
315                if (ex instanceof StaleObjectStateException) {
316                        StaleObjectStateException hibEx = (StaleObjectStateException) ex;
317                        return new ObjectOptimisticLockingFailureException(hibEx.getEntityName(), hibEx.getIdentifier(), ex);
318                }
319                if (ex instanceof StaleStateException) {
320                        return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex);
321                }
322                if (optimisticLockExceptionClass.isInstance(ex)) {
323                        return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex);
324                }
325                if (pessimisticLockExceptionClass != null && pessimisticLockExceptionClass.isInstance(ex)) {
326                        if (ex.getCause() instanceof LockAcquisitionException) {
327                                return new CannotAcquireLockException(ex.getMessage(), ex.getCause());
328                        }
329                        return new PessimisticLockingFailureException(ex.getMessage(), ex);
330                }
331
332                // fallback
333                return new JpaSystemException(ex);
334        }
335
336        protected Session getSession(EntityManager entityManager) {
337                return entityManager.unwrap(Session.class);
338        }
339
340
341        private static class SessionTransactionData {
342
343                private final Session session;
344
345                private final FlushMode previousFlushMode;
346
347                private final Connection preparedCon;
348
349                private final Integer previousIsolationLevel;
350
351                public SessionTransactionData(
352                                Session session, FlushMode previousFlushMode, Connection preparedCon, Integer previousIsolationLevel) {
353                        this.session = session;
354                        this.previousFlushMode = previousFlushMode;
355                        this.preparedCon = preparedCon;
356                        this.previousIsolationLevel = previousIsolationLevel;
357                }
358
359                public void resetSessionState() {
360                        if (this.previousFlushMode != null) {
361                                this.session.setFlushMode(this.previousFlushMode);
362                        }
363                        if (this.preparedCon != null && this.session.isConnected()) {
364                                Connection conToReset = HibernateConnectionHandle.doGetConnection(this.session);
365                                if (conToReset != this.preparedCon) {
366                                        LogFactory.getLog(HibernateJpaDialect.class).warn(
367                                                        "JDBC Connection to reset not identical to originally prepared Connection - please " +
368                                                        "make sure to use connection release mode ON_CLOSE (the default) and to run against " +
369                                                        "Hibernate 4.2+ (or switch HibernateJpaDialect's prepareConnection flag to false");
370                                }
371                                DataSourceUtils.resetConnectionAfterTransaction(conToReset, this.previousIsolationLevel);
372                        }
373                }
374        }
375
376
377        private static class HibernateConnectionHandle implements ConnectionHandle {
378
379                // This will find a corresponding method on Hibernate 3.x but not on 4.x
380                private static final Method sessionConnectionMethod =
381                                ClassUtils.getMethodIfAvailable(Session.class, "connection");
382
383                private static volatile Method connectionMethodToUse = sessionConnectionMethod;
384
385                private final Session session;
386
387                public HibernateConnectionHandle(Session session) {
388                        this.session = session;
389                }
390
391                @Override
392                public Connection getConnection() {
393                        return doGetConnection(this.session);
394                }
395
396                @Override
397                public void releaseConnection(Connection con) {
398                        if (sessionConnectionMethod != null) {
399                                // Need to explicitly call close() with Hibernate 3.x in order to allow
400                                // for eager release of the underlying physical Connection if necessary.
401                                // However, do not do this on Hibernate 4.2+ since it would return the
402                                // physical Connection to the pool right away, making it unusable for
403                                // further operations within the current transaction!
404                                JdbcUtils.closeConnection(con);
405                        }
406                }
407
408                public static Connection doGetConnection(Session session) {
409                        try {
410                                if (connectionMethodToUse == null) {
411                                        // Reflective lookup to find SessionImpl's connection() method on Hibernate 4.x
412                                        connectionMethodToUse = session.getClass().getMethod("connection");
413                                }
414                                return (Connection) ReflectionUtils.invokeMethod(connectionMethodToUse, session);
415                        }
416                        catch (NoSuchMethodException ex) {
417                                throw new IllegalStateException("Cannot find connection() method on Hibernate Session", ex);
418                        }
419                }
420        }
421
422}