001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.orm.toplink;
018:
019: import java.sql.Connection;
020: import java.sql.SQLException;
021:
022: import javax.sql.DataSource;
023:
024: import oracle.toplink.exceptions.DatabaseException;
025: import oracle.toplink.exceptions.TopLinkException;
026: import oracle.toplink.internal.databaseaccess.Accessor;
027: import oracle.toplink.internal.databaseaccess.DatabaseAccessor;
028: import oracle.toplink.sessions.Session;
029:
030: import org.springframework.beans.factory.InitializingBean;
031: import org.springframework.dao.DataAccessException;
032: import org.springframework.jdbc.datasource.ConnectionHolder;
033: import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport;
034: import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
035: import org.springframework.jdbc.support.SQLExceptionTranslator;
036: import org.springframework.transaction.CannotCreateTransactionException;
037: import org.springframework.transaction.TransactionDefinition;
038: import org.springframework.transaction.support.AbstractPlatformTransactionManager;
039: import org.springframework.transaction.support.DefaultTransactionStatus;
040: import org.springframework.transaction.support.ResourceTransactionManager;
041: import org.springframework.transaction.support.TransactionSynchronizationManager;
042:
043: /**
044: * {@link org.springframework.transaction.PlatformTransactionManager} implementation
045: * for a single TopLink {@link SessionFactory}. Binds a TopLink Session from the
046: * specified factory to the thread, potentially allowing for one thread-bound Session
047: * per factory. {@link SessionFactoryUtils} and {@link TopLinkTemplate} are aware
048: * of thread-bound Sessions and participate in such transactions automatically.
049: * Using either of those or going through <code>Session.getActiveUnitOfWork()</code> is
050: * required for TopLink access code supporting this transaction handling mechanism.
051: *
052: * <p>This transaction manager is appropriate for applications that use a single
053: * TopLink SessionFactory for transactional data access. JTA (usually through
054: * {@link org.springframework.transaction.jta.JtaTransactionManager}) is necessary
055: * for accessing multiple transactional resources within the same transaction.
056: * Note that you need to configure TopLink with an appropriate external transaction
057: * controller in order to make it participate in JTA transactions.
058: *
059: * <p>This transaction manager also supports direct DataSource access within a transaction
060: * (i.e. plain JDBC code working with the same DataSource), but only for transactions
061: * that are <i>not</i> marked as read-only. This allows for mixing services which
062: * access TopLink and services which use plain JDBC (without being aware of TopLink)!
063: * Application code needs to stick to the same simple Connection lookup pattern as
064: * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}
065: * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection}
066: * or going through a
067: * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}).
068: *
069: * <p>Note: To be able to register a DataSource's Connection for plain JDBC code,
070: * this instance needs to be aware of the DataSource ({@link #setDataSource}).
071: * The given DataSource should obviously match the one used by the given TopLink
072: * SessionFactory.
073: *
074: * <p>On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0
075: * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"}
076: * flag defaults to "false", though, as nested transactions will just apply to the
077: * JDBC Connection, not to the TopLink PersistenceManager and its cached objects.
078: * You can manually set the flag to "true" if you want to use nested transactions
079: * for JDBC access code which participates in TopLink transactions (provided that
080: * your JDBC driver supports Savepoints). <i>Note that TopLink itself does not
081: * support nested transactions! Hence, do not expect TopLink access code to
082: * semantically participate in a nested transaction.</i>
083: *
084: * <p>Thanks to Slavik Markovich for implementing the initial TopLink support prototype!
085: *
086: * @author Juergen Hoeller
087: * @author <a href="mailto:james.x.clark@oracle.com">James Clark</a>
088: * @since 1.2
089: * @see #setSessionFactory
090: * @see #setDataSource
091: * @see LocalSessionFactoryBean
092: * @see SessionFactoryUtils#getSession
093: * @see SessionFactoryUtils#releaseSession
094: * @see TopLinkTemplate
095: * @see oracle.toplink.sessions.Session#getActiveUnitOfWork()
096: * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
097: * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout
098: * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
099: * @see org.springframework.jdbc.core.JdbcTemplate
100: * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
101: * @see org.springframework.transaction.jta.JtaTransactionManager
102: */
103: public class TopLinkTransactionManager extends
104: AbstractPlatformTransactionManager implements
105: ResourceTransactionManager, InitializingBean {
106:
107: private SessionFactory sessionFactory;
108:
109: private DataSource dataSource;
110:
111: private boolean lazyDatabaseTransaction = false;
112:
113: private SQLExceptionTranslator jdbcExceptionTranslator;
114:
115: /**
116: * Create a new TopLinkTransactionManager instance.
117: * A SessionFactory has to be specified to be able to use it.
118: * @see #setSessionFactory
119: */
120: public TopLinkTransactionManager() {
121: }
122:
123: /**
124: * Create a new TopLinkTransactionManager instance.
125: * @param sessionFactory the TopLink SessionFactory to manage transactions for
126: */
127: public TopLinkTransactionManager(SessionFactory sessionFactory) {
128: this .sessionFactory = sessionFactory;
129: afterPropertiesSet();
130: }
131:
132: /**
133: * Set the the TopLink SessionFactory to manage transactions for.
134: * This will usually be a ServerSessionFactory.
135: * <p>The passed-in SessionFactory will be asked for a plain Session
136: * in case of a read-only transaction (where no active UnitOfWork is
137: * supposed to be available), and for a managed Session else (with an
138: * active UnitOfWork that will be committed by this transaction manager).
139: * @see ServerSessionFactory
140: * @see SessionFactory#createSession()
141: * @see SessionFactory#createManagedClientSession()
142: */
143: public void setSessionFactory(SessionFactory sessionFactory) {
144: this .sessionFactory = sessionFactory;
145: }
146:
147: /**
148: * Return the SessionFactory that this instance should manage transactions for.
149: */
150: public SessionFactory getSessionFactory() {
151: return this .sessionFactory;
152: }
153:
154: /**
155: * Set the JDBC DataSource that this instance should manage transactions for.
156: * The DataSource should match the one used by the TopLink SessionFactory:
157: * for example, you could specify the same JNDI DataSource for both.
158: * <p>A transactional JDBC Connection for this DataSource will be provided to
159: * application code accessing this DataSource directly via DataSourceUtils
160: * or JdbcTemplate. The Connection will be taken from the TopLink Session.
161: * <b>This will only happen for transactions that are <i>not</i> marked
162: * as read-only.</b> TopLink does not support database transactions for pure
163: * read-only operations on a Session (that is, without a UnitOfWork).
164: * <p>Note that you need to use a TopLink Session with a DatabaseAccessor
165: * to allow for exposing TopLink transactions as JDBC transactions. This is
166: * the case of all standard TopLink configurations.
167: * <p>The DataSource specified here should be the target DataSource to manage
168: * transactions for, not a TransactionAwareDataSourceProxy. Only data access
169: * code may work with TransactionAwareDataSourceProxy, while the transaction
170: * manager needs to work on the underlying target DataSource. If there's
171: * nevertheless a TransactionAwareDataSourceProxy passed in, it will be
172: * unwrapped to extract its target DataSource.
173: * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
174: * @see org.springframework.jdbc.datasource.DataSourceUtils
175: * @see org.springframework.jdbc.core.JdbcTemplate
176: */
177: public void setDataSource(DataSource dataSource) {
178: if (dataSource instanceof TransactionAwareDataSourceProxy) {
179: // If we got a TransactionAwareDataSourceProxy, we need to perform transactions
180: // for its underlying target DataSource, else data access code won't see
181: // properly exposed transactions (i.e. transactions for the target DataSource).
182: this .dataSource = ((TransactionAwareDataSourceProxy) dataSource)
183: .getTargetDataSource();
184: } else {
185: this .dataSource = dataSource;
186: }
187: }
188:
189: /**
190: * Return the JDBC DataSource that this instance manages transactions for.
191: */
192: public DataSource getDataSource() {
193: return this .dataSource;
194: }
195:
196: /**
197: * Set whether to lazily start a database transaction within a TopLink
198: * transaction.
199: * <p>By default, database transactions are started early. This allows
200: * for reusing the same JDBC Connection throughout an entire transaction,
201: * including read operations, and also for exposing TopLink transactions
202: * to JDBC access code (working on the same DataSource).
203: * <p>It is only recommended to switch this flag to "true" when no JDBC access
204: * code is involved in any of the transactions, and when it is acceptable to
205: * perform read operations outside of the transactional JDBC Connection.
206: * @see #setDataSource(javax.sql.DataSource)
207: * @see oracle.toplink.sessions.UnitOfWork#beginEarlyTransaction()
208: */
209: public void setLazyDatabaseTransaction(
210: boolean lazyDatabaseTransaction) {
211: this .lazyDatabaseTransaction = lazyDatabaseTransaction;
212: }
213:
214: /**
215: * Return whether to lazily start a database transaction within a TopLink
216: * transaction.
217: */
218: public boolean isLazyDatabaseTransaction() {
219: return this .lazyDatabaseTransaction;
220: }
221:
222: /**
223: * Set the JDBC exception translator for this transaction manager.
224: * <p>Applied to any SQLException root cause of a TopLink DatabaseException
225: * that is thrown on commit. The default is to rely on TopLink's native
226: * exception translation.
227: * @param jdbcExceptionTranslator the exception translator
228: * @see oracle.toplink.exceptions.DatabaseException
229: * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
230: * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator
231: * @see #setDataSource(javax.sql.DataSource)
232: */
233: public void setJdbcExceptionTranslator(
234: SQLExceptionTranslator jdbcExceptionTranslator) {
235: this .jdbcExceptionTranslator = jdbcExceptionTranslator;
236: }
237:
238: /**
239: * Return the JDBC exception translator for this transaction manager, if any.
240: */
241: public SQLExceptionTranslator getJdbcExceptionTranslator() {
242: return this .jdbcExceptionTranslator;
243: }
244:
245: public void afterPropertiesSet() {
246: if (getSessionFactory() == null) {
247: throw new IllegalArgumentException(
248: "Property 'sessionFactory' is required");
249: }
250: }
251:
252: public Object getResourceFactory() {
253: return getSessionFactory();
254: }
255:
256: protected Object doGetTransaction() {
257: TopLinkTransactionObject txObject = new TopLinkTransactionObject();
258: SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
259: .getResource(this .sessionFactory);
260: txObject.setSessionHolder(sessionHolder);
261: return txObject;
262: }
263:
264: protected boolean isExistingTransaction(Object transaction) {
265: TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction;
266: return (txObject.getSessionHolder() != null);
267: }
268:
269: protected void doBegin(Object transaction,
270: TransactionDefinition definition) {
271: Session session = null;
272:
273: try {
274: if (!definition.isReadOnly()) {
275: logger
276: .debug("Creating managed TopLink Session with active UnitOfWork for read-write transaction");
277: session = getSessionFactory()
278: .createManagedClientSession();
279: } else {
280: logger
281: .debug("Creating plain TopLink Session without active UnitOfWork for read-only transaction");
282: session = getSessionFactory().createSession();
283: }
284:
285: if (logger.isDebugEnabled()) {
286: logger.debug("Opened new session [" + session
287: + "] for TopLink transaction");
288: }
289:
290: TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction;
291: txObject.setSessionHolder(new SessionHolder(session));
292: txObject.getSessionHolder().setSynchronizedWithTransaction(
293: true);
294:
295: // Check isolation level.
296: switch (definition.getIsolationLevel()) {
297: case TransactionDefinition.ISOLATION_READ_UNCOMMITTED:
298: // TODO warn when queries are executed without the conformResultsInUnitOfWork setting
299: break;
300: case TransactionDefinition.ISOLATION_REPEATABLE_READ:
301: // TODO warn when queries are executed against a read-only Session
302: break;
303: case TransactionDefinition.ISOLATION_SERIALIZABLE:
304: // TODO warn if the TransactionIsolation settings on the DatabaseLogin are wrong
305: break;
306: }
307:
308: // Register transaction timeout.
309: int timeout = determineTimeout(definition);
310: if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
311: txObject.getSessionHolder()
312: .setTimeoutInSeconds(timeout);
313: }
314:
315: // Enforce early database transaction for TopLink read-write transaction,
316: // unless we are explicitly told to use lazy transactions.
317: if (!definition.isReadOnly()
318: && !isLazyDatabaseTransaction()) {
319: session.getActiveUnitOfWork().beginEarlyTransaction();
320: }
321:
322: // Register the TopLink Session's JDBC Connection for the DataSource, if set.
323: if (getDataSource() != null) {
324: Connection con = getJdbcConnection(session);
325: if (con != null) {
326: ConnectionHolder conHolder = new ConnectionHolder(
327: con);
328: if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
329: conHolder.setTimeoutInSeconds(timeout);
330: }
331: if (logger.isDebugEnabled()) {
332: logger
333: .debug("Exposing TopLink transaction as JDBC transaction ["
334: + con + "]");
335: }
336: TransactionSynchronizationManager.bindResource(
337: getDataSource(), conHolder);
338: txObject.setConnectionHolder(conHolder);
339: } else {
340: if (logger.isDebugEnabled()) {
341: logger
342: .debug("Not exposing TopLink transaction ["
343: + session
344: + "] as JDBC transaction because no JDBC Connection could be retrieved from it");
345: }
346: }
347: }
348:
349: // Bind the session holder to the thread.
350: TransactionSynchronizationManager.bindResource(
351: getSessionFactory(), txObject.getSessionHolder());
352: }
353:
354: catch (Exception ex) {
355: SessionFactoryUtils.releaseSession(session,
356: getSessionFactory());
357: throw new CannotCreateTransactionException(
358: "Could not open TopLink Session for transaction",
359: ex);
360: }
361: }
362:
363: /**
364: * Extract the underlying JDBC Connection from the given TopLink Session.
365: * <p>Default implementation casts to <code>oracle.toplink.publicinterface.Session</code>
366: * and fetches the Connection from the DatabaseAccessor exposed there.
367: * @param session the current TopLink Session
368: * @return the underlying JDBC Connection, or <code>null</code> if none found
369: * @see oracle.toplink.publicinterface.Session#getAccessor()
370: * @see oracle.toplink.internal.databaseaccess.DatabaseAccessor#getConnection()
371: */
372: protected Connection getJdbcConnection(Session session) {
373: if (!(session instanceof oracle.toplink.publicinterface.Session)) {
374: if (logger.isDebugEnabled()) {
375: logger
376: .debug("TopLink Session ["
377: + session
378: + "] does not derive from [oracle.toplink.publicinterface.Session]");
379: }
380: return null;
381: }
382: Accessor accessor = ((oracle.toplink.publicinterface.Session) session)
383: .getAccessor();
384: if (!(accessor instanceof DatabaseAccessor)) {
385: if (logger.isDebugEnabled()) {
386: logger
387: .debug("TopLink Accessor ["
388: + accessor
389: + "] does not derive from [oracle.toplink.internal.databaseaccess.DatabaseAccessor]");
390: }
391: return null;
392: }
393: return ((DatabaseAccessor) accessor).getConnection();
394: }
395:
396: protected Object doSuspend(Object transaction) {
397: TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction;
398: txObject.setSessionHolder(null);
399: return TransactionSynchronizationManager
400: .unbindResource(getSessionFactory());
401: }
402:
403: protected void doResume(Object transaction,
404: Object suspendedResources) {
405: SessionHolder sessionHolder = (SessionHolder) suspendedResources;
406: if (TransactionSynchronizationManager
407: .hasResource(getSessionFactory())) {
408: // From non-transactional code running in active transaction synchronization
409: // -> can be safely removed, will be closed on transaction completion.
410: TransactionSynchronizationManager
411: .unbindResource(getSessionFactory());
412: }
413: TransactionSynchronizationManager.bindResource(
414: getSessionFactory(), sessionHolder);
415: }
416:
417: protected void doCommit(DefaultTransactionStatus status) {
418: TopLinkTransactionObject txObject = (TopLinkTransactionObject) status
419: .getTransaction();
420: if (status.isDebug()) {
421: logger.debug("Committing TopLink transaction on session ["
422: + txObject.getSessionHolder().getSession() + "]");
423: }
424: try {
425: if (!status.isReadOnly()) {
426: txObject.getSessionHolder().getSession()
427: .getActiveUnitOfWork().commit();
428: }
429: txObject.getSessionHolder().clear();
430: } catch (TopLinkException ex) {
431: throw convertTopLinkAccessException(ex);
432: }
433: }
434:
435: protected void doRollback(DefaultTransactionStatus status) {
436: TopLinkTransactionObject txObject = (TopLinkTransactionObject) status
437: .getTransaction();
438: if (status.isDebug()) {
439: logger
440: .debug("Not committing TopLink transaction on session ["
441: + txObject.getSessionHolder().getSession()
442: + "]");
443: }
444: txObject.getSessionHolder().clear();
445: }
446:
447: protected void doSetRollbackOnly(DefaultTransactionStatus status) {
448: TopLinkTransactionObject txObject = (TopLinkTransactionObject) status
449: .getTransaction();
450: if (status.isDebug()) {
451: logger.debug("Setting TopLink transaction on session ["
452: + txObject.getSessionHolder().getSession()
453: + "] rollback-only");
454: }
455: txObject.getSessionHolder().setRollbackOnly();
456: }
457:
458: protected void doCleanupAfterCompletion(Object transaction) {
459: TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction;
460:
461: // Remove the session holder from the thread.
462: TransactionSynchronizationManager
463: .unbindResource(getSessionFactory());
464:
465: // Remove the JDBC connection holder from the thread, if exposed.
466: if (txObject.hasConnectionHolder()) {
467: TransactionSynchronizationManager
468: .unbindResource(getDataSource());
469: }
470:
471: Session session = txObject.getSessionHolder().getSession();
472: if (logger.isDebugEnabled()) {
473: logger.debug("Releasing TopLink Session [" + session
474: + "] after transaction");
475: }
476: try {
477: session.release();
478: } catch (Throwable ex) {
479: // just log it, to keep a transaction-related exception
480: logger
481: .debug(
482: "Could not release TopLink Session after transaction",
483: ex);
484: }
485: }
486:
487: /**
488: * Convert the given TopLinkException to an appropriate exception from the
489: * <code>org.springframework.dao</code> hierarchy.
490: * <p>Will automatically apply a specified SQLExceptionTranslator to a
491: * TopLink DatabaseException, else rely on TopLink's default translation.
492: * @param ex TopLinkException that occured
493: * @return a corresponding DataAccessException
494: * @see SessionFactoryUtils#convertTopLinkAccessException
495: * @see #setJdbcExceptionTranslator
496: */
497: protected DataAccessException convertTopLinkAccessException(
498: TopLinkException ex) {
499: if (getJdbcExceptionTranslator() != null
500: && ex instanceof DatabaseException) {
501: Throwable internalEx = ex.getInternalException();
502: // Should always be a SQLException inside a DatabaseException.
503: if (internalEx instanceof SQLException) {
504: return getJdbcExceptionTranslator().translate(
505: "TopLink commit: " + ex.getMessage(), null,
506: (SQLException) internalEx);
507: }
508: }
509: return SessionFactoryUtils.convertTopLinkAccessException(ex);
510: }
511:
512: /**
513: * TopLink transaction object, representing a SessionHolder.
514: * Used as transaction object by TopLinkTransactionManager.
515: *
516: * <p>Derives from JdbcTransactionObjectSupport in order to inherit the
517: * capability to manage JDBC 3.0 Savepoints for underlying JDBC Connections.
518: */
519: private static class TopLinkTransactionObject extends
520: JdbcTransactionObjectSupport {
521:
522: private SessionHolder sessionHolder;
523:
524: public void setSessionHolder(SessionHolder sessionHolder) {
525: this .sessionHolder = sessionHolder;
526: }
527:
528: public SessionHolder getSessionHolder() {
529: return this .sessionHolder;
530: }
531:
532: public boolean isRollbackOnly() {
533: return getSessionHolder().isRollbackOnly();
534: }
535: }
536:
537: }
|