1 /*
2 * Copyright 2002-2009 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.orm.hibernate3;
18
19 import java.sql.Connection;
20 import javax.sql.DataSource;
21
22 import org.hibernate.ConnectionReleaseMode;
23 import org.hibernate.FlushMode;
24 import org.hibernate.HibernateException;
25 import org.hibernate.Interceptor;
26 import org.hibernate.JDBCException;
27 import org.hibernate.Session;
28 import org.hibernate.SessionFactory;
29 import org.hibernate.Transaction;
30 import org.hibernate.exception.GenericJDBCException;
31 import org.hibernate.impl.SessionImpl;
32
33 import org.springframework.beans.BeansException;
34 import org.springframework.beans.factory.BeanFactory;
35 import org.springframework.beans.factory.BeanFactoryAware;
36 import org.springframework.beans.factory.InitializingBean;
37 import org.springframework.dao.DataAccessException;
38 import org.springframework.dao.DataAccessResourceFailureException;
39 import org.springframework.jdbc.datasource.ConnectionHolder;
40 import org.springframework.jdbc.datasource.DataSourceUtils;
41 import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport;
42 import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
43 import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
44 import org.springframework.jdbc.support.SQLExceptionTranslator;
45 import org.springframework.transaction.CannotCreateTransactionException;
46 import org.springframework.transaction.IllegalTransactionStateException;
47 import org.springframework.transaction.InvalidIsolationLevelException;
48 import org.springframework.transaction.TransactionDefinition;
49 import org.springframework.transaction.TransactionSystemException;
50 import org.springframework.transaction.support.AbstractPlatformTransactionManager;
51 import org.springframework.transaction.support.DefaultTransactionStatus;
52 import org.springframework.transaction.support.ResourceTransactionManager;
53 import org.springframework.transaction.support.TransactionSynchronizationManager;
54
55 /**
56 * {@link org.springframework.transaction.PlatformTransactionManager}
57 * implementation for a single Hibernate {@link org.hibernate.SessionFactory}.
58 * Binds a Hibernate Session from the specified factory to the thread, potentially
59 * allowing for one thread-bound Session per factory. {@link SessionFactoryUtils}
60 * and {@link HibernateTemplate} are aware of thread-bound Sessions and participate
61 * in such transactions automatically. Using either of those or going through
62 * <code>SessionFactory.getCurrentSession()</code> is required for Hibernate
63 * access code that needs to support this transaction handling mechanism.
64 *
65 * <p>Supports custom isolation levels, and timeouts that get applied as
66 * Hibernate transaction timeouts.
67 *
68 * <p>This transaction manager is appropriate for applications that use a single
69 * Hibernate SessionFactory for transactional data access, but it also supports
70 * direct DataSource access within a transaction (i.e. plain JDBC code working
71 * with the same DataSource). This allows for mixing services which access Hibernate
72 * and services which use plain JDBC (without being aware of Hibernate)!
73 * Application code needs to stick to the same simple Connection lookup pattern as
74 * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}
75 * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection}
76 * or going through a
77 * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}).
78 *
79 * <p>Note: To be able to register a DataSource's Connection for plain JDBC code,
80 * this instance needs to be aware of the DataSource ({@link #setDataSource}).
81 * The given DataSource should obviously match the one used by the given
82 * SessionFactory. To achieve this, configure both to the same JNDI DataSource,
83 * or preferably create the SessionFactory with {@link LocalSessionFactoryBean} and
84 * a local DataSource (which will be autodetected by this transaction manager).
85 *
86 * <p>JTA (usually through {@link org.springframework.transaction.jta.JtaTransactionManager})
87 * is necessary for accessing multiple transactional resources within the same
88 * transaction. The DataSource that Hibernate uses needs to be JTA-enabled in
89 * such a scenario (see container setup). Normally, JTA setup for Hibernate is
90 * somewhat container-specific due to the JTA TransactionManager lookup, required
91 * for proper transactional handling of the SessionFactory-level read-write cache.
92 *
93 * <p>Fortunately, there is an easier way with Spring: {@link SessionFactoryUtils}
94 * (and thus {@link HibernateTemplate}) registers synchronizations with Spring's
95 * {@link org.springframework.transaction.support.TransactionSynchronizationManager}
96 * (as used by {@link org.springframework.transaction.jta.JtaTransactionManager}),
97 * for proper after-completion callbacks. Therefore, as long as Spring's
98 * JtaTransactionManager drives the JTA transactions, Hibernate does not require
99 * any special configuration for proper JTA participation. Note that there are
100 * special restrictions with EJB CMT and restrictive JTA subsystems: See
101 * {@link org.springframework.transaction.jta.JtaTransactionManager}'s javadoc for details.
102 *
103 * <p>On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0
104 * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"}
105 * flag defaults to "false", though, as nested transactions will just apply to the
106 * JDBC Connection, not to the Hibernate Session and its cached objects. You can
107 * manually set the flag to "true" if you want to use nested transactions for
108 * JDBC access code which participates in Hibernate transactions (provided that
109 * your JDBC driver supports Savepoints). <i>Note that Hibernate itself does not
110 * support nested transactions! Hence, do not expect Hibernate access code to
111 * semantically participate in a nested transaction.</i>
112 *
113 * <p>Requires Hibernate 3.2 or later, as of Spring 3.0.
114 *
115 * @author Juergen Hoeller
116 * @since 1.2
117 * @see #setSessionFactory
118 * @see #setDataSource
119 * @see LocalSessionFactoryBean
120 * @see SessionFactoryUtils#getSession
121 * @see SessionFactoryUtils#applyTransactionTimeout
122 * @see SessionFactoryUtils#releaseSession
123 * @see HibernateTemplate
124 * @see org.hibernate.SessionFactory#getCurrentSession()
125 * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
126 * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout
127 * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
128 * @see org.springframework.jdbc.core.JdbcTemplate
129 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
130 * @see org.springframework.transaction.jta.JtaTransactionManager
131 */
132 public class HibernateTransactionManager extends AbstractPlatformTransactionManager
133 implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {
134
135 private SessionFactory sessionFactory;
136
137 private DataSource dataSource;
138
139 private boolean autodetectDataSource = true;
140
141 private boolean prepareConnection = true;
142
143 private boolean hibernateManagedSession = false;
144
145 private boolean earlyFlushBeforeCommit = false;
146
147 private Object entityInterceptor;
148
149 private SQLExceptionTranslator jdbcExceptionTranslator;
150
151 private SQLExceptionTranslator defaultJdbcExceptionTranslator;
152
153 /**
154 * Just needed for entityInterceptorBeanName.
155 * @see #setEntityInterceptorBeanName
156 */
157 private BeanFactory beanFactory;
158
159
160 /**
161 * Create a new HibernateTransactionManager instance.
162 * A SessionFactory has to be set to be able to use it.
163 * @see #setSessionFactory
164 */
165 public HibernateTransactionManager() {
166 }
167
168 /**
169 * Create a new HibernateTransactionManager instance.
170 * @param sessionFactory SessionFactory to manage transactions for
171 */
172 public HibernateTransactionManager(SessionFactory sessionFactory) {
173 this.sessionFactory = sessionFactory;
174 afterPropertiesSet();
175 }
176
177
178 /**
179 * Set the SessionFactory that this instance should manage transactions for.
180 */
181 public void setSessionFactory(SessionFactory sessionFactory) {
182 this.sessionFactory = sessionFactory;
183 }
184
185 /**
186 * Return the SessionFactory that this instance should manage transactions for.
187 */
188 public SessionFactory getSessionFactory() {
189 return this.sessionFactory;
190 }
191
192 /**
193 * Set the JDBC DataSource that this instance should manage transactions for.
194 * The DataSource should match the one used by the Hibernate SessionFactory:
195 * for example, you could specify the same JNDI DataSource for both.
196 * <p>If the SessionFactory was configured with LocalDataSourceConnectionProvider,
197 * i.e. by Spring's LocalSessionFactoryBean with a specified "dataSource",
198 * the DataSource will be auto-detected: You can still explictly specify the
199 * DataSource, but you don't need to in this case.
200 * <p>A transactional JDBC Connection for this DataSource will be provided to
201 * application code accessing this DataSource directly via DataSourceUtils
202 * or JdbcTemplate. The Connection will be taken from the Hibernate Session.
203 * <p>The DataSource specified here should be the target DataSource to manage
204 * transactions for, not a TransactionAwareDataSourceProxy. Only data access
205 * code may work with TransactionAwareDataSourceProxy, while the transaction
206 * manager needs to work on the underlying target DataSource. If there's
207 * nevertheless a TransactionAwareDataSourceProxy passed in, it will be
208 * unwrapped to extract its target DataSource.
209 * @see #setAutodetectDataSource
210 * @see LocalDataSourceConnectionProvider
211 * @see LocalSessionFactoryBean#setDataSource
212 * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
213 * @see org.springframework.jdbc.datasource.DataSourceUtils
214 * @see org.springframework.jdbc.core.JdbcTemplate
215 */
216 public void setDataSource(DataSource dataSource) {
217 if (dataSource instanceof TransactionAwareDataSourceProxy) {
218 // If we got a TransactionAwareDataSourceProxy, we need to perform transactions
219 // for its underlying target DataSource, else data access code won't see
220 // properly exposed transactions (i.e. transactions for the target DataSource).
221 this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
222 }
223 else {
224 this.dataSource = dataSource;
225 }
226 }
227
228 /**
229 * Return the JDBC DataSource that this instance manages transactions for.
230 */
231 public DataSource getDataSource() {
232 return this.dataSource;
233 }
234
235 /**
236 * Set whether to autodetect a JDBC DataSource used by the Hibernate SessionFactory,
237 * if set via LocalSessionFactoryBean's <code>setDataSource</code>. Default is "true".
238 * <p>Can be turned off to deliberately ignore an available DataSource, in order
239 * to not expose Hibernate transactions as JDBC transactions for that DataSource.
240 * @see #setDataSource
241 * @see LocalSessionFactoryBean#setDataSource
242 */
243 public void setAutodetectDataSource(boolean autodetectDataSource) {
244 this.autodetectDataSource = autodetectDataSource;
245 }
246
247 /**
248 * Set whether to prepare the underlying JDBC Connection of a transactional
249 * Hibernate Session, that is, whether to apply a transaction-specific
250 * isolation level and/or the transaction's read-only flag to the underlying
251 * JDBC Connection.
252 * <p>Default is "true". If you turn this flag off, the transaction manager
253 * will not support per-transaction isolation levels anymore. It will not
254 * call <code>Connection.setReadOnly(true)</code> for read-only transactions
255 * anymore either. If this flag is turned off, no cleanup of a JDBC Connection
256 * is required after a transaction, since no Connection settings will get modified.
257 * @see java.sql.Connection#setTransactionIsolation
258 * @see java.sql.Connection#setReadOnly
259 */
260 public void setPrepareConnection(boolean prepareConnection) {
261 this.prepareConnection = prepareConnection;
262 }
263
264 /**
265 * Set whether to operate on a Hibernate-managed Session instead of a
266 * Spring-managed Session, that is, whether to obtain the Session through
267 * Hibernate's {@link org.hibernate.SessionFactory#getCurrentSession()}
268 * instead of {@link org.hibernate.SessionFactory#openSession()} (with a Spring
269 * {@link org.springframework.transaction.support.TransactionSynchronizationManager}
270 * check preceding it).
271 * <p>Default is "false", i.e. using a Spring-managed Session: taking the current
272 * thread-bound Session if available (e.g. in an Open-Session-in-View scenario),
273 * creating a new Session for the current transaction otherwise.
274 * <p>Switch this flag to "true" in order to enforce use of a Hibernate-managed Session.
275 * Note that this requires {@link org.hibernate.SessionFactory#getCurrentSession()}
276 * to always return a proper Session when called for a Spring-managed transaction;
277 * transaction begin will fail if the <code>getCurrentSession()</code> call fails.
278 * <p>This mode will typically be used in combination with a custom Hibernate
279 * {@link org.hibernate.context.CurrentSessionContext} implementation that stores
280 * Sessions in a place other than Spring's TransactionSynchronizationManager.
281 * It may also be used in combination with Spring's Open-Session-in-View support
282 * (using Spring's default {@link SpringSessionContext}), in which case it subtly
283 * differs from the Spring-managed Session mode: The pre-bound Session will <i>not</i>
284 * receive a <code>clear()</code> call (on rollback) or a <code>disconnect()</code>
285 * call (on transaction completion) in such a scenario; this is rather left up
286 * to a custom CurrentSessionContext implementation (if desired).
287 */
288 public void setHibernateManagedSession(boolean hibernateManagedSession) {
289 this.hibernateManagedSession = hibernateManagedSession;
290 }
291
292 /**
293 * Set whether to perform an early flush before proceeding with a commit.
294 * <p>Default is "false", performing an implicit flush as part of the actual
295 * commit step. Switch this to "true" in order to enforce an explicit early
296 * flush right <i>before</i> the actual commit step.
297 * <p>An early flush happens before the before-commit synchronization phase,
298 * making flushed state visible to <code>beforeCommit</code> callbacks of registered
299 * {@link org.springframework.transaction.support.TransactionSynchronization}
300 * objects. Such explicit flush behavior is consistent with Spring-driven
301 * flushing in a JTA transaction environment, so may also get enforced for
302 * consistency with JTA transaction behavior.
303 * @see #prepareForCommit
304 */
305 public void setEarlyFlushBeforeCommit(boolean earlyFlushBeforeCommit) {
306 this.earlyFlushBeforeCommit = earlyFlushBeforeCommit;
307 }
308
309 /**
310 * Set the bean name of a Hibernate entity interceptor that allows to inspect
311 * and change property values before writing to and reading from the database.
312 * Will get applied to any new Session created by this transaction manager.
313 * <p>Requires the bean factory to be known, to be able to resolve the bean
314 * name to an interceptor instance on session creation. Typically used for
315 * prototype interceptors, i.e. a new interceptor instance per session.
316 * <p>Can also be used for shared interceptor instances, but it is recommended
317 * to set the interceptor reference directly in such a scenario.
318 * @param entityInterceptorBeanName the name of the entity interceptor in
319 * the bean factory
320 * @see #setBeanFactory
321 * @see #setEntityInterceptor
322 */
323 public void setEntityInterceptorBeanName(String entityInterceptorBeanName) {
324 this.entityInterceptor = entityInterceptorBeanName;
325 }
326
327 /**
328 * Set a Hibernate entity interceptor that allows to inspect and change
329 * property values before writing to and reading from the database.
330 * Will get applied to any new Session created by this transaction manager.
331 * <p>Such an interceptor can either be set at the SessionFactory level,
332 * i.e. on LocalSessionFactoryBean, or at the Session level, i.e. on
333 * HibernateTemplate, HibernateInterceptor, and HibernateTransactionManager.
334 * It's preferable to set it on LocalSessionFactoryBean or HibernateTransactionManager
335 * to avoid repeated configuration and guarantee consistent behavior in transactions.
336 * @see LocalSessionFactoryBean#setEntityInterceptor
337 * @see HibernateTemplate#setEntityInterceptor
338 * @see HibernateInterceptor#setEntityInterceptor
339 */
340 public void setEntityInterceptor(Interceptor entityInterceptor) {
341 this.entityInterceptor = entityInterceptor;
342 }
343
344 /**
345 * Return the current Hibernate entity interceptor, or <code>null</code> if none.
346 * Resolves an entity interceptor bean name via the bean factory,
347 * if necessary.
348 * @throws IllegalStateException if bean name specified but no bean factory set
349 * @throws BeansException if bean name resolution via the bean factory failed
350 * @see #setEntityInterceptor
351 * @see #setEntityInterceptorBeanName
352 * @see #setBeanFactory
353 */
354 public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException {
355 if (this.entityInterceptor instanceof Interceptor) {
356 return (Interceptor) entityInterceptor;
357 }
358 else if (this.entityInterceptor instanceof String) {
359 if (this.beanFactory == null) {
360 throw new IllegalStateException("Cannot get entity interceptor via bean name if no bean factory set");
361 }
362 String beanName = (String) this.entityInterceptor;
363 return this.beanFactory.getBean(beanName, Interceptor.class);
364 }
365 else {
366 return null;
367 }
368 }
369
370 /**
371 * Set the JDBC exception translator for this transaction manager.
372 * <p>Applied to any SQLException root cause of a Hibernate JDBCException that
373 * is thrown on flush, overriding Hibernate's default SQLException translation
374 * (which is based on Hibernate's Dialect for a specific target database).
375 * @param jdbcExceptionTranslator the exception translator
376 * @see java.sql.SQLException
377 * @see org.hibernate.JDBCException
378 * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
379 * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator
380 */
381 public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) {
382 this.jdbcExceptionTranslator = jdbcExceptionTranslator;
383 }
384
385 /**
386 * Return the JDBC exception translator for this transaction manager, if any.
387 */
388 public SQLExceptionTranslator getJdbcExceptionTranslator() {
389 return this.jdbcExceptionTranslator;
390 }
391
392 /**
393 * The bean factory just needs to be known for resolving entity interceptor
394 * bean names. It does not need to be set for any other mode of operation.
395 * @see #setEntityInterceptorBeanName
396 */
397 public void setBeanFactory(BeanFactory beanFactory) {
398 this.beanFactory = beanFactory;
399 }
400
401 public void afterPropertiesSet() {
402 if (getSessionFactory() == null) {
403 throw new IllegalArgumentException("Property 'sessionFactory' is required");
404 }
405 if (this.entityInterceptor instanceof String && this.beanFactory == null) {
406 throw new IllegalArgumentException("Property 'beanFactory' is required for 'entityInterceptorBeanName'");
407 }
408
409 // Check for SessionFactory's DataSource.
410 if (this.autodetectDataSource && getDataSource() == null) {
411 DataSource sfds = SessionFactoryUtils.getDataSource(getSessionFactory());
412 if (sfds != null) {
413 // Use the SessionFactory's DataSource for exposing transactions to JDBC code.
414 if (logger.isInfoEnabled()) {
415 logger.info("Using DataSource [" + sfds +
416 "] of Hibernate SessionFactory for HibernateTransactionManager");
417 }
418 setDataSource(sfds);
419 }
420 }
421 }
422
423
424 public Object getResourceFactory() {
425 return getSessionFactory();
426 }
427
428 @Override
429 protected Object doGetTransaction() {
430 HibernateTransactionObject txObject = new HibernateTransactionObject();
431 txObject.setSavepointAllowed(isNestedTransactionAllowed());
432
433 SessionHolder sessionHolder =
434 (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory());
435 if (sessionHolder != null) {
436 if (logger.isDebugEnabled()) {
437 logger.debug("Found thread-bound Session [" +
438 SessionFactoryUtils.toString(sessionHolder.getSession()) + "] for Hibernate transaction");
439 }
440 txObject.setSessionHolder(sessionHolder);
441 }
442 else if (this.hibernateManagedSession) {
443 try {
444 Session session = getSessionFactory().getCurrentSession();
445 if (logger.isDebugEnabled()) {
446 logger.debug("Found Hibernate-managed Session [" +
447 SessionFactoryUtils.toString(session) + "] for Spring-managed transaction");
448 }
449 txObject.setExistingSession(session);
450 }
451 catch (HibernateException ex) {
452 throw new DataAccessResourceFailureException(
453 "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex);
454 }
455 }
456
457 if (getDataSource() != null) {
458 ConnectionHolder conHolder = (ConnectionHolder)
459 TransactionSynchronizationManager.getResource(getDataSource());
460 txObject.setConnectionHolder(conHolder);
461 }
462
463 return txObject;
464 }
465
466 @Override
467 protected boolean isExistingTransaction(Object transaction) {
468 HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
469 return (txObject.hasSpringManagedTransaction() ||
470 (this.hibernateManagedSession && txObject.hasHibernateManagedTransaction()));
471 }
472
473 @Override
474 protected void doBegin(Object transaction, TransactionDefinition definition) {
475 HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
476
477 if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
478 throw new IllegalTransactionStateException(
479 "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " +
480 "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
481 "It is recommended to use a single HibernateTransactionManager for all transactions " +
482 "on a single DataSource, no matter whether Hibernate or JDBC access.");
483 }
484
485 Session session = null;
486
487 try {
488 if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
489 Interceptor entityInterceptor = getEntityInterceptor();
490 Session newSession = (entityInterceptor != null ?
491 getSessionFactory().openSession(entityInterceptor) : getSessionFactory().openSession());
492 if (logger.isDebugEnabled()) {
493 logger.debug("Opened new Session [" + SessionFactoryUtils.toString(newSession) +
494 "] for Hibernate transaction");
495 }
496 txObject.setSession(newSession);
497 }
498
499 session = txObject.getSessionHolder().getSession();
500
501 if (this.prepareConnection && isSameConnectionForEntireSession(session)) {
502 // We're allowed to change the transaction settings of the JDBC Connection.
503 if (logger.isDebugEnabled()) {
504 logger.debug(
505 "Preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
506 }
507 Connection con = session.connection();
508 Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
509 txObject.setPreviousIsolationLevel(previousIsolationLevel);
510 }
511 else {
512 // Not allowed to change the transaction settings of the JDBC Connection.
513 if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
514 // We should set a specific isolation level but are not allowed to...
515 throw new InvalidIsolationLevelException(
516 "HibernateTransactionManager is not allowed to support custom isolation levels: " +
517 "make sure that its 'prepareConnection' flag is on (the default) and that the " +
518 "Hibernate connection release mode is set to 'on_close' (SpringTransactionFactory's default). " +
519 "Make sure that your LocalSessionFactoryBean actually uses SpringTransactionFactory: Your " +
520 "Hibernate properties should *not* include a 'hibernate.transaction.factory_class' property!");
521 }
522 if (logger.isDebugEnabled()) {
523 logger.debug(
524 "Not preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
525 }
526 }
527
528 if (definition.isReadOnly() && txObject.isNewSession()) {
529 // Just set to NEVER in case of a new Session for this transaction.
530 session.setFlushMode(FlushMode.MANUAL);
531 }
532
533 if (!definition.isReadOnly() && !txObject.isNewSession()) {
534 // We need AUTO or COMMIT for a non-read-only transaction.
535 FlushMode flushMode = session.getFlushMode();
536 if (flushMode.lessThan(FlushMode.COMMIT)) {
537 session.setFlushMode(FlushMode.AUTO);
538 txObject.getSessionHolder().setPreviousFlushMode(flushMode);
539 }
540 }
541
542 Transaction hibTx;
543
544 // Register transaction timeout.
545 int timeout = determineTimeout(definition);
546 if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
547 // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+
548 // Applies to all statements, also to inserts, updates and deletes!
549 hibTx = session.getTransaction();
550 hibTx.setTimeout(timeout);
551 hibTx.begin();
552 }
553 else {
554 // Open a plain Hibernate transaction without specified timeout.
555 hibTx = session.beginTransaction();
556 }
557
558 // Add the Hibernate transaction to the session holder.
559 txObject.getSessionHolder().setTransaction(hibTx);
560
561 // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
562 if (getDataSource() != null) {
563 Connection con = session.connection();
564 ConnectionHolder conHolder = new ConnectionHolder(con);
565 if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
566 conHolder.setTimeoutInSeconds(timeout);
567 }
568 if (logger.isDebugEnabled()) {
569 logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]");
570 }
571 TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
572 txObject.setConnectionHolder(conHolder);
573 }
574
575 // Bind the session holder to the thread.
576 if (txObject.isNewSessionHolder()) {
577 TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder());
578 }
579 txObject.getSessionHolder().setSynchronizedWithTransaction(true);
580 }
581
582 catch (Exception ex) {
583 if (txObject.isNewSession()) {
584 try {
585 if (session.getTransaction().isActive()) {
586 session.getTransaction().rollback();
587 }
588 }
589 catch (Throwable ex2) {
590 logger.debug("Could not rollback Session after failed transaction begin", ex);
591 }
592 finally {
593 SessionFactoryUtils.closeSession(session);
594 }
595 }
596 throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
597 }
598 }
599
600 @Override
601 protected Object doSuspend(Object transaction) {
602 HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
603 txObject.setSessionHolder(null);
604 SessionHolder sessionHolder =
605 (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory());
606 txObject.setConnectionHolder(null);
607 ConnectionHolder connectionHolder = null;
608 if (getDataSource() != null) {
609 connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource());
610 }
611 return new SuspendedResourcesHolder(sessionHolder, connectionHolder);
612 }
613
614 @Override
615 protected void doResume(Object transaction, Object suspendedResources) {
616 SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources;
617 if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
618 // From non-transactional code running in active transaction synchronization
619 // -> can be safely removed, will be closed on transaction completion.
620 TransactionSynchronizationManager.unbindResource(getSessionFactory());
621 }
622 TransactionSynchronizationManager.bindResource(getSessionFactory(), resourcesHolder.getSessionHolder());
623 if (getDataSource() != null) {
624 TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder());
625 }
626 }
627
628 @Override
629 protected void prepareForCommit(DefaultTransactionStatus status) {
630 if (this.earlyFlushBeforeCommit && status.isNewTransaction()) {
631 HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
632 Session session = txObject.getSessionHolder().getSession();
633 if (!session.getFlushMode().lessThan(FlushMode.COMMIT)) {
634 logger.debug("Performing an early flush for Hibernate transaction");
635 try {
636 session.flush();
637 }
638 catch (HibernateException ex) {
639 throw convertHibernateAccessException(ex);
640 }
641 finally {
642 session.setFlushMode(FlushMode.MANUAL);
643 }
644 }
645 }
646 }
647
648 @Override
649 protected void doCommit(DefaultTransactionStatus status) {
650 HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
651 if (status.isDebug()) {
652 logger.debug("Committing Hibernate transaction on Session [" +
653 SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "]");
654 }
655 try {
656 txObject.getSessionHolder().getTransaction().commit();
657 }
658 catch (org.hibernate.TransactionException ex) {
659 // assumably from commit call to the underlying JDBC connection
660 throw new TransactionSystemException("Could not commit Hibernate transaction", ex);
661 }
662 catch (HibernateException ex) {
663 // assumably failed to flush changes to database
664 throw convertHibernateAccessException(ex);
665 }
666 }
667
668 @Override
669 protected void doRollback(DefaultTransactionStatus status) {
670 HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
671 if (status.isDebug()) {
672 logger.debug("Rolling back Hibernate transaction on Session [" +
673 SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "]");
674 }
675 try {
676 txObject.getSessionHolder().getTransaction().rollback();
677 }
678 catch (org.hibernate.TransactionException ex) {
679 throw new TransactionSystemException("Could not roll back Hibernate transaction", ex);
680 }
681 catch (HibernateException ex) {
682 // Shouldn't really happen, as a rollback doesn't cause a flush.
683 throw convertHibernateAccessException(ex);
684 }
685 finally {
686 if (!txObject.isNewSession() && !this.hibernateManagedSession) {
687 // Clear all pending inserts/updates/deletes in the Session.
688 // Necessary for pre-bound Sessions, to avoid inconsistent state.
689 txObject.getSessionHolder().getSession().clear();
690 }
691 }
692 }
693
694 @Override
695 protected void doSetRollbackOnly(DefaultTransactionStatus status) {
696 HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
697 if (status.isDebug()) {
698 logger.debug("Setting Hibernate transaction on Session [" +
699 SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "] rollback-only");
700 }
701 txObject.setRollbackOnly();
702 }
703
704 @Override
705 protected void doCleanupAfterCompletion(Object transaction) {
706 HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
707
708 // Remove the session holder from the thread.
709 if (txObject.isNewSessionHolder()) {
710 TransactionSynchronizationManager.unbindResource(getSessionFactory());
711 }
712
713 // Remove the JDBC connection holder from the thread, if exposed.
714 if (getDataSource() != null) {
715 TransactionSynchronizationManager.unbindResource(getDataSource());
716 }
717
718 Session session = txObject.getSessionHolder().getSession();
719 if (this.prepareConnection && session.isConnected() && isSameConnectionForEntireSession(session)) {
720 // We're running with connection release mode "on_close": We're able to reset
721 // the isolation level and/or read-only flag of the JDBC Connection here.
722 // Else, we need to rely on the connection pool to perform proper cleanup.
723 try {
724 Connection con = session.connection();
725 DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
726 }
727 catch (HibernateException ex) {
728 logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
729 }
730 }
731
732 if (txObject.isNewSession()) {
733 if (logger.isDebugEnabled()) {
734 logger.debug("Closing Hibernate Session [" + SessionFactoryUtils.toString(session) +
735 "] after transaction");
736 }
737 SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory());
738 }
739 else {
740 if (logger.isDebugEnabled()) {
741 logger.debug("Not closing pre-bound Hibernate Session [" +
742 SessionFactoryUtils.toString(session) + "] after transaction");
743 }
744 if (txObject.getSessionHolder().getPreviousFlushMode() != null) {
745 session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode());
746 }
747 if (!this.hibernateManagedSession) {
748 session.disconnect();
749 }
750 }
751 txObject.getSessionHolder().clear();
752 }
753
754 /**
755 * Return whether the given Hibernate Session will always hold the same
756 * JDBC Connection. This is used to check whether the transaction manager
757 * can safely prepare and clean up the JDBC Connection used for a transaction.
758 * <p>Default implementation checks the Session's connection release mode
759 * to be "on_close". Unfortunately, this requires casting to SessionImpl,
760 * as of Hibernate 3.1. If that cast doesn't work, we'll simply assume
761 * we're safe and return <code>true</code>.
762 * @param session the Hibernate Session to check
763 * @see org.hibernate.impl.SessionImpl#getConnectionReleaseMode()
764 * @see org.hibernate.ConnectionReleaseMode#ON_CLOSE
765 */
766 protected boolean isSameConnectionForEntireSession(Session session) {
767 if (!(session instanceof SessionImpl)) {
768 // The best we can do is to assume we're safe.
769 return true;
770 }
771 ConnectionReleaseMode releaseMode = ((SessionImpl) session).getConnectionReleaseMode();
772 return ConnectionReleaseMode.ON_CLOSE.equals(releaseMode);
773 }
774
775
776 /**
777 * Convert the given HibernateException to an appropriate exception
778 * from the <code>org.springframework.dao</code> hierarchy.
779 * <p>Will automatically apply a specified SQLExceptionTranslator to a
780 * Hibernate JDBCException, else rely on Hibernate's default translation.
781 * @param ex HibernateException that occured
782 * @return a corresponding DataAccessException
783 * @see SessionFactoryUtils#convertHibernateAccessException
784 * @see #setJdbcExceptionTranslator
785 */
786 protected DataAccessException convertHibernateAccessException(HibernateException ex) {
787 if (getJdbcExceptionTranslator() != null && ex instanceof JDBCException) {
788 return convertJdbcAccessException((JDBCException) ex, getJdbcExceptionTranslator());
789 }
790 else if (GenericJDBCException.class.equals(ex.getClass())) {
791 return convertJdbcAccessException((GenericJDBCException) ex, getDefaultJdbcExceptionTranslator());
792 }
793 return SessionFactoryUtils.convertHibernateAccessException(ex);
794 }
795
796 /**
797 * Convert the given Hibernate JDBCException to an appropriate exception
798 * from the <code>org.springframework.dao</code> hierarchy, using the
799 * given SQLExceptionTranslator.
800 * @param ex Hibernate JDBCException that occured
801 * @param translator the SQLExceptionTranslator to use
802 * @return a corresponding DataAccessException
803 */
804 protected DataAccessException convertJdbcAccessException(JDBCException ex, SQLExceptionTranslator translator) {
805 return translator.translate("Hibernate flushing: " + ex.getMessage(), ex.getSQL(), ex.getSQLException());
806 }
807
808 /**
809 * Obtain a default SQLExceptionTranslator, lazily creating it if necessary.
810 * <p>Creates a default
811 * {@link org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator}
812 * for the SessionFactory's underlying DataSource.
813 */
814 protected synchronized SQLExceptionTranslator getDefaultJdbcExceptionTranslator() {
815 if (this.defaultJdbcExceptionTranslator == null) {
816 if (getDataSource() != null) {
817 this.defaultJdbcExceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(getDataSource());
818 }
819 else {
820 this.defaultJdbcExceptionTranslator = SessionFactoryUtils.newJdbcExceptionTranslator(getSessionFactory());
821 }
822 }
823 return this.defaultJdbcExceptionTranslator;
824 }
825
826
827 /**
828 * Hibernate transaction object, representing a SessionHolder.
829 * Used as transaction object by HibernateTransactionManager.
830 */
831 private class HibernateTransactionObject extends JdbcTransactionObjectSupport {
832
833 private SessionHolder sessionHolder;
834
835 private boolean newSessionHolder;
836
837 private boolean newSession;
838
839 public void setSession(Session session) {
840 this.sessionHolder = new SessionHolder(session);
841 this.newSessionHolder = true;
842 this.newSession = true;
843 }
844
845 public void setExistingSession(Session session) {
846 this.sessionHolder = new SessionHolder(session);
847 this.newSessionHolder = true;
848 this.newSession = false;
849 }
850
851 public void setSessionHolder(SessionHolder sessionHolder) {
852 this.sessionHolder = sessionHolder;
853 this.newSessionHolder = false;
854 this.newSession = false;
855 }
856
857 public SessionHolder getSessionHolder() {
858 return this.sessionHolder;
859 }
860
861 public boolean isNewSessionHolder() {
862 return this.newSessionHolder;
863 }
864
865 public boolean isNewSession() {
866 return this.newSession;
867 }
868
869 public boolean hasSpringManagedTransaction() {
870 return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
871 }
872
873 public boolean hasHibernateManagedTransaction() {
874 return (this.sessionHolder != null && this.sessionHolder.getSession().getTransaction().isActive());
875 }
876
877 public void setRollbackOnly() {
878 this.sessionHolder.setRollbackOnly();
879 if (hasConnectionHolder()) {
880 getConnectionHolder().setRollbackOnly();
881 }
882 }
883
884 public boolean isRollbackOnly() {
885 return this.sessionHolder.isRollbackOnly() ||
886 (hasConnectionHolder() && getConnectionHolder().isRollbackOnly());
887 }
888
889 public void flush() {
890 try {
891 this.sessionHolder.getSession().flush();
892 }
893 catch (HibernateException ex) {
894 throw convertHibernateAccessException(ex);
895 }
896 }
897 }
898
899
900 /**
901 * Holder for suspended resources.
902 * Used internally by <code>doSuspend</code> and <code>doResume</code>.
903 */
904 private static class SuspendedResourcesHolder {
905
906 private final SessionHolder sessionHolder;
907
908 private final ConnectionHolder connectionHolder;
909
910 private SuspendedResourcesHolder(SessionHolder sessionHolder, ConnectionHolder conHolder) {
911 this.sessionHolder = sessionHolder;
912 this.connectionHolder = conHolder;
913 }
914
915 private SessionHolder getSessionHolder() {
916 return this.sessionHolder;
917 }
918
919 private ConnectionHolder getConnectionHolder() {
920 return this.connectionHolder;
921 }
922 }
923
924 }