com.avaje.ebeaninternal.server.transaction.JdbcTransaction Maven / Gradle / Ivy
/**
* Copyright (C) 2006 Robin Bygrave
*
* This file is part of Ebean.
*
* Ebean is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Ebean is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Ebean; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.avaje.ebeaninternal.server.transaction;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import com.avaje.ebean.LogLevel;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.config.lucene.IndexUpdateFuture;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.api.TransactionEvent;
import com.avaje.ebeaninternal.server.lucene.LIndexUpdateFuture;
import com.avaje.ebeaninternal.server.lucene.PersistenceLuceneException;
import com.avaje.ebeaninternal.server.persist.BatchControl;
import com.avaje.ebeaninternal.server.transaction.TransactionManager.OnQueryOnly;
/**
* JDBC Connection based transaction.
*/
public class JdbcTransaction implements SpiTransaction {
private static final Logger logger = Logger.getLogger(JdbcTransaction.class.getName());
private static final String illegalStateMessage = "Transaction is Inactive";
/**
* The associated TransactionManager.
*/
final protected TransactionManager manager;
/**
* The transaction id.
*/
final String id;
/**
* Flag to indicate if this was an explicitly created Transaction.
*/
final boolean explicit;
/**
* Behaviour for ending query only transactions.
*/
final OnQueryOnly onQueryOnly;
/**
* The status of the transaction.
*/
boolean active;
/**
* The underlying Connection.
*/
Connection connection;
/**
* Used to queue up persist requests for batch execution.
*/
BatchControl batchControl;
/**
* The event which holds persisted beans.
*/
TransactionEvent event;
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
PersistenceContext persistenceContext;
/**
* Used to give developers more control over the insert update and delete
* functionality.
*/
boolean persistCascade = true;
/**
* Flag used for performance to skip commit or rollback of query only
* transactions in read committed transaction isolation.
*/
boolean queryOnly = true;
boolean localReadOnly;
LogLevel logLevel;
/**
* Set to true if using batch processing.
*/
boolean batchMode;
int batchSize = -1;
boolean batchFlushOnQuery = true;
Boolean batchGetGeneratedKeys;
Boolean batchFlushOnMixed;
/**
* The depth used by batch processing to help the ordering
* of statements.
*/
int depth = 0;
HashSet persistingBeans;
TransactionLogBuffer logBuffer;
List indexUpdateFutures;
/**
* Create a new JdbcTransaction.
*/
public JdbcTransaction(String id, boolean explicit, LogLevel logLevel, Connection connection, TransactionManager manager) {
try {
this.active = true;
this.id = id;
this.explicit = explicit;
this.logLevel = logLevel;
this.manager = manager;
this.connection = connection;
this.onQueryOnly = manager == null ? OnQueryOnly.ROLLBACK : manager.getOnQueryOnly();
this.persistenceContext = new DefaultPersistenceContext();
this.logBuffer = new TransactionLogBuffer(50, id);
} catch (Exception e) {
throw new PersistenceException(e);
}
}
public String toString() {
return "Trans["+id+"]";
}
public void addIndexUpdateFuture(LIndexUpdateFuture future) {
if (indexUpdateFutures == null){
indexUpdateFutures = new ArrayList();
}
indexUpdateFutures.add(future);
}
public void waitForIndexUpdates() {
if (indexUpdateFutures != null){
try {
for (IndexUpdateFuture f : indexUpdateFutures) {
f.get();
}
} catch (InterruptedException e) {
throw new PersistenceLuceneException(e);
} catch (ExecutionException e) {
throw new PersistenceLuceneException(e);
}
}
}
/**
* Add a bean to the registed list.
*
* This is to handle bi-directional relationships where
* both sides Cascade.
*
*/
public void registerBean(Integer persistingBean) {
if (persistingBeans == null){
persistingBeans = new HashSet();
}
persistingBeans.add(persistingBean);
}
/**
* Unregister the persisted bean.
*/
public void unregisterBean(Integer persistedBean) {
if (persistingBeans != null){
persistingBeans.remove(persistedBean);
}
}
/**
* Return true if this is a bean that has already been saved/deleted.
*/
public boolean isRegisteredBean(Integer persistingBean) {
if (persistingBeans == null){
return false;
} else {
return persistingBeans.contains(persistingBean);
}
}
/**
* Return the depth of the current persist request plus the diff.
* This has the effect of changing the current depth and returning
* the new value. Pass diff=0 to return the current depth.
*
* The depth of 0 is for the initial persist request. It is
* modified as the cascading of the save or delete traverses to
* the the associated Ones (-1) and associated Manys (+1).
*
*
* The depth is used to help the ordering of batched statements.
*
*
* @param diff the amount to add or subtract from the depth.
* @return the current depth plus the diff
*/
public int depth(int diff) {
depth += diff;
return depth;
}
public boolean isReadOnly() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
return connection.isReadOnly();
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
public void setReadOnly(boolean readOnly) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
localReadOnly = readOnly;
connection.setReadOnly(readOnly);
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
public void setBatchMode(boolean batchMode) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.batchMode = batchMode;
}
public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) {
this.batchGetGeneratedKeys = getGeneratedKeys;
if (batchControl != null){
batchControl.setGetGeneratedKeys(getGeneratedKeys);
}
}
public void setBatchFlushOnMixed(boolean batchFlushOnMixed) {
this.batchFlushOnMixed = batchFlushOnMixed;
if (batchControl != null){
batchControl.setBatchFlushOnMixed(batchFlushOnMixed);
}
}
/**
* Return the batchSize specifically set for this transaction or 0.
*
* Returning 0 implies to use the system wide default batch size.
*
*/
public int getBatchSize() {
return batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
if (batchControl != null){
batchControl.setBatchSize(batchSize);
}
}
public boolean isBatchFlushOnQuery() {
return batchFlushOnQuery;
}
public void setBatchFlushOnQuery(boolean batchFlushOnQuery) {
this.batchFlushOnQuery = batchFlushOnQuery;
}
/**
* Return true if this request should be batched. Returning false
* means that this request should be executed immediately.
*/
public boolean isBatchThisRequest() {
if (!explicit && depth <= 0){
// implicit transaction ... no gain
// by batching where depth <= 0
return false;
}
return batchMode;
}
public BatchControl getBatchControl(){
return batchControl;
}
/**
* Set the BatchControl to the transaction.
* This is done once per transaction on the first persist request.
*/
public void setBatchControl(BatchControl batchControl){
queryOnly = false;
this.batchControl = batchControl;
// in case these parameters have already been set
if (batchGetGeneratedKeys != null){
batchControl.setGetGeneratedKeys(batchGetGeneratedKeys);
}
if (batchSize != -1) {
batchControl.setBatchSize(batchSize);
}
if (batchFlushOnMixed != null){
batchControl.setBatchFlushOnMixed(batchFlushOnMixed);
}
}
/**
* Flush any queued persist requests.
*
* This is general will result in a number of batched
* PreparedStatements executing.
*
*/
public void flushBatch() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
if (batchControl != null){
batchControl.flush();
}
}
public void batchFlush() {
flushBatch();
}
/**
* Return the persistence context associated with this transaction.
*/
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
/**
* Set the persistence context to this transaction.
*
* This could be considered similar to EJB3 Extended PersistanceContext. In
* that you get the PersistanceContext from a transaction, hold onto it, and
* then set it back later to a second transaction.
*
*/
public void setPersistenceContext(PersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.persistenceContext = context;
}
/**
* Return the underlying TransactionEvent.
*/
public TransactionEvent getEvent() {
queryOnly = false;
if (event == null){
event = new TransactionEvent();
}
return event;
}
/**
* Is transaction logging on for this transaction.
*/
public boolean isLoggingOn() {
return logLevel.ordinal() > LogLevel.NONE.ordinal();
}
/**
* Set whether transaction logging is on for this transaction.
*/
public void setLoggingOn(boolean loggingOn) {
if (loggingOn){
logLevel = LogLevel.SQL;
} else {
logLevel = LogLevel.NONE;
}
}
/**
* Return true if this was an explicitly created transaction.
*/
public boolean isExplicit() {
return explicit;
}
public boolean isLogSql() {
return logLevel.ordinal() >= LogLevel.SQL.ordinal();
}
public boolean isLogSummary() {
return logLevel.ordinal() >= LogLevel.SUMMARY.ordinal();
}
public LogLevel getLogLevel() {
return logLevel;
}
public void setLogLevel(LogLevel logLevel) {
this.logLevel = logLevel;
}
/**
* Log a message to the transaction log.
*/
public void log(String msg) {
if (isLoggingOn() && manager != null) {
if (logBuffer.add(msg)) {
// buffer full so flush it
manager.log(logBuffer);
logBuffer = logBuffer.newBuffer();
}
}
}
/**
* Return the transaction id.
*/
public String getId() {
return id;
}
/**
* Return the underlying connection for internal use.
*/
public Connection getInternalConnection() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
return connection;
}
/**
* Return the underlying connection for public use.
*/
public Connection getConnection() {
queryOnly = false;
return getInternalConnection();
}
protected void deactivate() {
try {
if (localReadOnly){
connection.setReadOnly(false);
}
} catch (SQLException e){
logger.log(Level.SEVERE, "Error setting to readOnly?", e);
}
try {
connection.close();
} catch (Exception ex) {
// the connection pool will automatically remove the
// connection if it does not pass the test
logger.log(Level.SEVERE, "Error closing connection", ex);
}
connection = null;
active = false;
}
public TransactionLogBuffer getLogBuffer() {
return logBuffer;
}
/**
* Notify the transaction manager.
*/
protected void notifyCommit() {
if (manager == null){
return;
}
if (queryOnly){
manager.notifyOfQueryOnly(true, this, null);
} else {
manager.notifyOfCommit(this);
}
}
/**
* Rollback, Commit or Close for query only transaction.
*
* For a transaction that was used for queries only we can choose
* to either rollback or just close the connection for performance.
*
*/
private void commitQueryOnly() {
try {
switch (onQueryOnly) {
case ROLLBACK:
connection.rollback();
break;
case COMMIT:
connection.commit();
break;
case CLOSE_ON_READCOMMITTED:
// Connection is closed via deactivate() which follows
// This optimisation is only available at READ COMMITTED Isolation
break;
default:
connection.rollback();
}
} catch (SQLException e) {
String m = "Error when ending a query only transaction via " + onQueryOnly;
logger.log(Level.SEVERE, m, e);
}
}
/**
* Commit the transaction.
*/
public void commit() throws RollbackException {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
if (queryOnly) {
// can rollback or just close for performance
commitQueryOnly();
} else {
// commit
if (batchControl != null && !batchControl.isEmpty()){
batchControl.flush();
}
connection.commit();
}
// these will not throw an exception
deactivate();
notifyCommit();
} catch (Exception e) {
throw new RollbackException(e);
}
}
/**
* Notify the transaction manager.
*/
protected void notifyRollback(Throwable cause) {
if (manager == null){
return;
}
if (queryOnly){
manager.notifyOfQueryOnly(false, this, cause);
} else {
manager.notifyOfRollback(this, cause);
}
}
/**
* Rollback the transaction.
*/
public void rollback() throws PersistenceException {
rollback(null);
}
/**
* Rollback the transaction.
* If there is a throwable it is logged as the cause in the transaction log.
*/
public void rollback(Throwable cause) throws PersistenceException {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
connection.rollback();
// these will not throw an exception
deactivate();
notifyRollback(cause);
} catch (Exception ex) {
throw new PersistenceException(ex);
}
}
/**
* If the transaction is active then perform rollback.
*/
public void end() throws PersistenceException {
if (isActive()) {
rollback();
}
}
/**
* Return true if the transaction is active.
*/
public boolean isActive() {
return active;
}
public boolean isPersistCascade() {
return persistCascade;
}
public void setPersistCascade(boolean persistCascade) {
this.persistCascade = persistCascade;
}
public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) {
getEvent().add(tableName, inserts, updates, deletes);
}
public final TransactionManager getTransactionManger(){
return manager;
}
}