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