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}