All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.avaje.ebeaninternal.server.transaction.JdbcTransaction Maven / Gradle / Ivy
package com.avaje.ebeaninternal.server.transaction;
import com.avaje.ebean.TransactionCallback;
import com.avaje.ebean.annotation.DocStoreMode;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.config.PersistBatch;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebeaninternal.api.DerivedRelationshipData;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.api.TransactionEvent;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.lib.util.Str;
import com.avaje.ebeaninternal.server.persist.BatchControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* JDBC Connection based transaction.
*/
public class JdbcTransaction implements SpiTransaction {
private static final Logger logger = LoggerFactory.getLogger(JdbcTransaction.class);
private static final Object PLACEHOLDER = new Object();
private static final String illegalStateMessage = "Transaction is Inactive";
/**
* The associated TransactionManager.
*/
protected final TransactionManager manager;
/**
* The transaction id.
*/
protected final String id;
/**
* Flag to indicate if this was an explicitly created Transaction.
*/
protected final boolean explicit;
/**
* Behaviour for ending query only transactions.
*/
protected final OnQueryOnly onQueryOnly;
/**
* The status of the transaction.
*/
protected boolean active;
/**
* The underlying Connection.
*/
protected Connection connection;
/**
* Used to queue up persist requests for batch execution.
*/
protected BatchControl batchControl;
/**
* The event which holds persisted beans.
*/
protected TransactionEvent event;
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
protected PersistenceContext persistenceContext;
/**
* Used to give developers more control over the insert update and delete
* functionality.
*/
protected boolean persistCascade = true;
/**
* Flag used for performance to skip commit or rollback of query only
* transactions in read committed transaction isolation.
*/
protected boolean queryOnly = true;
protected boolean localReadOnly;
protected Boolean updateAllLoadedProperties;
protected PersistBatch oldBatchMode;
protected PersistBatch batchMode;
protected PersistBatch batchOnCascadeMode;
protected int batchSize = -1;
protected boolean batchFlushOnQuery = true;
protected Boolean batchGetGeneratedKeys;
protected Boolean batchFlushOnMixed;
protected String logPrefix;
/**
* The depth used by batch processing to help the ordering of statements.
*/
protected int depth;
/**
* Set to true if the connection has autoCommit=true initially.
*/
protected boolean autoCommit;
protected IdentityHashMap persistingBeans;
protected HashSet deletingBeansHash;
protected HashMap m2mIntersectionSave;
protected HashMap> derivedRelMap;
protected Map userObjects;
protected List callbackList;
protected boolean batchOnCascadeSet;
protected TChangeLogHolder changeLogHolder;
/**
* The mode for updating doc store indexes for this transaction.
* Only set when you want to override the default behavior.
*/
protected DocStoreMode docStoreMode;
protected int docStoreBatchSize;
/**
* Explicit control over skipCache.
*/
protected Boolean skipCache;
/**
* Default skip cache behavior from {@link ServerConfig#isSkipCacheAfterWrite()}.
*/
protected final boolean skipCacheAfterWrite;
/**
* Create a new JdbcTransaction.
*/
public JdbcTransaction(String id, boolean explicit, Connection connection, TransactionManager manager) {
try {
this.active = true;
this.id = id;
this.logPrefix = deriveLogPrefix(id);
this.explicit = explicit;
this.manager = manager;
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
if (manager == null) {
this.skipCacheAfterWrite = true;
this.batchMode = PersistBatch.NONE;
this.batchOnCascadeMode = PersistBatch.NONE;
this.onQueryOnly = OnQueryOnly.ROLLBACK;
} else {
this.skipCacheAfterWrite = manager.isSkipCacheAfterWrite();
this.batchMode = manager.getPersistBatch();
this.batchOnCascadeMode = manager.getPersistBatchOnCascade();
this.onQueryOnly = manager.getOnQueryOnly();
}
checkAutoCommit(connection);
} catch (Exception e) {
throw new PersistenceException(e);
}
}
/**
* Overridden in AutoCommitJdbcTransaction as that expects to run/operate with autocommit true.
*/
protected void checkAutoCommit(Connection connection) throws SQLException {
if (connection != null) {
this.autoCommit = connection.getAutoCommit();
if (this.autoCommit) {
connection.setAutoCommit(false);
}
}
}
private static String deriveLogPrefix(String id) {
StringBuilder sb = new StringBuilder();
sb.append("txn[");
if (id != null) {
sb.append(id);
}
sb.append("] ");
return sb.toString();
}
@Override
public boolean isSkipCache() {
if (skipCache != null) return skipCache;
return skipCacheAfterWrite && !queryOnly;
}
@Override
public void setSkipCache(boolean skipCache) {
this.skipCache = skipCache;
}
@Override
public String getLogPrefix() {
return logPrefix;
}
public String toString() {
return logPrefix;
}
@Override
public void addBeanChange(BeanChange beanChange) {
if (changeLogHolder == null) {
changeLogHolder = new TChangeLogHolder(this, 100);
}
changeLogHolder.addBeanChange(beanChange);
}
@Override
public void sendChangeLog(ChangeSet changesRequest) {
manager.sendChangeLog(changesRequest);
}
@Override
public void register(TransactionCallback callback) {
if (callbackList == null) {
callbackList = new ArrayList(4);
}
callbackList.add(callback);
}
protected void firePreRollback() {
if (callbackList != null) {
for (TransactionCallback callback : callbackList) {
try {
callback.preRollback();
} catch (Exception e) {
logger.error("Error executing preRollback callback", e);
}
}
}
}
protected void firePostRollback() {
if (callbackList != null) {
for (TransactionCallback callback : callbackList) {
try {
callback.postRollback();
} catch (Exception e) {
logger.error("Error executing postRollback callback", e);
}
}
}
if (changeLogHolder != null) {
changeLogHolder.postRollback();
}
}
protected void firePreCommit() {
if (callbackList != null) {
for (TransactionCallback callback : callbackList) {
try {
callback.preCommit();
} catch (Exception e) {
logger.error("Error executing preCommit callback", e);
}
}
}
}
protected void firePostCommit() {
if (callbackList != null) {
for (TransactionCallback callback : callbackList) {
try {
callback.postCommit();
} catch (Exception e) {
logger.error("Error executing postCommit callback", e);
}
}
}
if (changeLogHolder != null) {
changeLogHolder.postCommit();
}
}
@Override
public int getDocStoreBatchSize() {
return docStoreBatchSize;
}
@Override
public void setDocStoreBatchSize(int docStoreBatchSize) {
this.docStoreBatchSize = docStoreBatchSize;
}
public DocStoreMode getDocStoreMode() {
return docStoreMode;
}
@Override
public void setDocStoreMode(DocStoreMode docStoreMode) {
this.docStoreMode = docStoreMode;
}
@Override
public List getDerivedRelationship(Object bean) {
if (derivedRelMap == null) {
return null;
}
return derivedRelMap.get(System.identityHashCode(bean));
}
@Override
public void registerDerivedRelationship(DerivedRelationshipData derivedRelationship) {
if (derivedRelMap == null) {
derivedRelMap = new HashMap>();
}
Integer key = System.identityHashCode(derivedRelationship.getAssocBean());
List list = derivedRelMap.get(key);
if (list == null) {
list = new ArrayList();
derivedRelMap.put(key, list);
}
list.add(derivedRelationship);
}
/**
* Add a bean to the registed list.
*
* This is to handle bi-directional relationships where both sides Cascade.
*
*/
@Override
public void registerDeleteBean(Integer persistingBean) {
if (deletingBeansHash == null) {
deletingBeansHash = new HashSet();
}
deletingBeansHash.add(persistingBean);
}
/**
* Unregister the persisted bean.
*/
@Override
public void unregisterDeleteBean(Integer persistedBean) {
if (deletingBeansHash != null) {
deletingBeansHash.remove(persistedBean);
}
}
/**
* Return true if this is a bean that has already been saved/deleted.
*/
@Override
public boolean isRegisteredDeleteBean(Integer persistingBean) {
return deletingBeansHash != null && deletingBeansHash.contains(persistingBean);
}
/**
* Unregister the persisted bean.
*/
@Override
public void unregisterBean(Object bean) {
persistingBeans.remove(bean);
}
/**
* Return true if this is a bean that has already been saved. This will
* register the bean if it is not already.
*/
@Override
public boolean isRegisteredBean(Object bean) {
if (persistingBeans == null) {
persistingBeans = new IdentityHashMap();
}
return (persistingBeans.put(bean, PLACEHOLDER) != null);
}
/**
* Return true if the m2m intersection save is allowed from a given bean direction.
* This is to stop m2m intersection management via both directions of a m2m.
*/
@Override
public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) {
if (m2mIntersectionSave == null) {
// first attempt so yes allow this m2m intersection direction
m2mIntersectionSave = new HashMap();
m2mIntersectionSave.put(intersectionTable, beanName);
return true;
}
String existingBean = m2mIntersectionSave.get(intersectionTable);
if (existingBean == null) {
// first time into this intersection table so allow
m2mIntersectionSave.put(intersectionTable, beanName);
return true;
}
// only allow if save coming from the same bean type
// to stop saves coming from both directions of m2m
return existingBean.equals(beanName);
}
/**
* 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.
*/
@Override
public void depth(int diff) {
depth += diff;
}
/**
* Return the current depth.
*/
@Override
public int depth() {
return depth;
}
@Override
public void markNotQueryOnly() {
this.queryOnly = false;
}
@Override
public boolean isReadOnly() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
return connection.isReadOnly();
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
@Override
public void setReadOnly(boolean readOnly) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
localReadOnly = readOnly;
connection.setReadOnly(readOnly);
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
@Override
public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) {
this.updateAllLoadedProperties = updateAllLoadedProperties;
}
public Boolean isUpdateAllLoadedProperties() {
return updateAllLoadedProperties;
}
@Override
public void setBatchMode(boolean batchMode) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.batchMode = (batchMode) ? PersistBatch.ALL : PersistBatch.NONE;
}
@Override
public void setBatch(PersistBatch batchMode) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.batchMode = batchMode;
}
@Override
public PersistBatch getBatch() {
return batchMode;
}
@Override
public void setBatchOnCascade(PersistBatch batchOnCascadeMode) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.batchOnCascadeMode = batchOnCascadeMode;
}
@Override
public PersistBatch getBatchOnCascade() {
return batchOnCascadeMode;
}
@Override
public Boolean getBatchGetGeneratedKeys() {
return batchGetGeneratedKeys;
}
@Override
public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) {
this.batchGetGeneratedKeys = getGeneratedKeys;
if (batchControl != null) {
batchControl.setGetGeneratedKeys(getGeneratedKeys);
}
}
@Override
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.
*
*/
@Override
public int getBatchSize() {
return batchSize;
}
@Override
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
if (batchControl != null) {
batchControl.setBatchSize(batchSize);
}
}
@Override
public boolean isBatchFlushOnQuery() {
return batchFlushOnQuery;
}
@Override
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.
*/
@Override
public boolean isBatchThisRequest(PersistRequest.Type type) {
if (!batchOnCascadeSet && !explicit && depth <= 0) {
// implicit transaction, no gain by batching where depth <= 0
return false;
}
return isBatch(batchMode, type);
}
/**
* Return true if JDBC batch should be used on cascade persist.
*/
private boolean isBatchOnCascade(PersistRequest.Type type) {
return isBatch(batchOnCascadeMode, type);
}
private boolean isBatch(PersistBatch batch, PersistRequest.Type type) {
switch (batch) {
case ALL:
return true;
case INSERT:
return type == PersistRequest.Type.INSERT;
default:
return false;
}
}
public void checkBatchEscalationOnCollection() {
if (batchMode == PersistBatch.NONE && batchOnCascadeMode != PersistBatch.NONE) {
batchMode = batchOnCascadeMode;
batchOnCascadeSet = true;
}
}
public void flushBatchOnCollection() {
if (batchOnCascadeSet) {
if (batchControl != null) {
if (logger.isTraceEnabled()) {
logger.trace("... flushBatchOnCollection");
}
batchControl.flushReset();
}
// restore the previous batch mode of NONE
batchMode = PersistBatch.NONE;
}
}
/**
* Flush after completing persist cascade.
*/
@Override
public void flushBatchOnCascade() {
if (batchControl != null) {
if (logger.isTraceEnabled()) {
logger.trace("... flushBatchOnCascade");
}
batchControl.flushReset();
}
// restore the previous batch mode
batchMode = oldBatchMode;
}
public boolean checkBatchEscalationOnCascade(PersistRequestBean> request) {
if (isBatch(batchMode, request.getType())) {
// already batching (at top level)
return false;
}
if (isBatchOnCascade(request.getType())) {
// escalate up to batch mode for this request (and cascade)
oldBatchMode = batchMode;
batchMode = PersistBatch.ALL;
if (batchControl != null) {
// flush with reset so that this request goes into it's own batch buffer
batchControl.flushReset();
}
// skip using jdbc batch for the top level bean (no gain there)
request.setSkipBatchForTopLevel();
return true;
}
if (batchControl != null && !batchControl.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace("... flush from batchOnCascade ");
}
batchControl.flushReset();
}
return false;
}
@Override
public BatchControl getBatchControl() {
return batchControl;
}
/**
* Set the BatchControl to the transaction. This is done once per transaction
* on the first persist request.
*/
@Override
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.
*
*/
@Override
public void flushBatch() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
if (batchControl != null) {
batchControl.flush();
}
}
/**
* Return the persistence context associated with this transaction.
*/
@Override
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.
*
*/
@Override
public void setPersistenceContext(PersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.persistenceContext = context;
}
/**
* Return the underlying TransactionEvent.
*/
@Override
public TransactionEvent getEvent() {
queryOnly = false;
if (event == null) {
event = new TransactionEvent();
}
return event;
}
/**
* Return true if this was an explicitly created transaction.
*/
@Override
public boolean isExplicit() {
return explicit;
}
@Override
public boolean isLogSql() {
return TransactionManager.SQL_LOGGER.isDebugEnabled();
}
@Override
public boolean isLogSummary() {
return TransactionManager.SUM_LOGGER.isDebugEnabled();
}
@Override
public void logSql(String msg) {
TransactionManager.SQL_LOGGER.debug(Str.add(logPrefix, msg));
}
@Override
public void logSummary(String msg) {
TransactionManager.SUM_LOGGER.debug(Str.add(logPrefix, msg));
}
/**
* Return the transaction id.
*/
@Override
public String getId() {
return id;
}
/**
* Return the underlying connection for internal use.
*/
@Override
public Connection getInternalConnection() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
return connection;
}
/**
* Return the underlying connection for public use.
*/
@Override
public Connection getConnection() {
queryOnly = false;
return getInternalConnection();
}
protected void deactivate() {
try {
if (localReadOnly) {
// reset readOnly status prior to returning to pool
connection.setReadOnly(false);
}
} catch (SQLException e) {
logger.error("Error setting to readOnly?", e);
}
try {
if (autoCommit) {
// reset the autoCommit status prior to returning to pool
connection.setAutoCommit(true);
}
} catch (SQLException e) {
logger.error("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.error("Error closing connection", ex);
}
connection = null;
active = false;
}
/**
* Notify the transaction manager.
*/
protected void notifyCommit() {
if (manager != null) {
if (queryOnly) {
manager.notifyOfQueryOnly(this);
} else {
manager.notifyOfCommit(this);
}
}
}
protected void notifyQueryOnly() {
if (manager != null) {
manager.notifyOfQueryOnly(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.
*
*/
protected void connectionEndForQueryOnly() {
try {
switch (onQueryOnly) {
case ROLLBACK:
performRollback();
break;
case COMMIT:
performCommit();
break;
case CLOSE:
// valid at READ COMMITTED Isolation
break;
default:
performRollback();
}
} catch (SQLException e) {
logger.error("Error when ending a query only transaction via " + onQueryOnly, e);
}
}
/**
* Perform the actual rollback on the connection.
*/
protected void performRollback() throws SQLException {
connection.rollback();
}
/**
* Perform the actual commit on the connection.
*/
protected void performCommit() throws SQLException {
connection.commit();
}
/**
* Commit the transaction.
*/
@Override
public void commit() throws RollbackException {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
firePreCommit();
try {
if (queryOnly) {
// can rollback or just close for performance
connectionEndForQueryOnly();
} else {
// commit
if (batchControl != null && !batchControl.isEmpty()) {
batchControl.flush();
}
performCommit();
}
} catch (Exception e) {
throw new RollbackException(e);
} finally {
// these will not throw an exception
firePostCommit();
deactivate();
notifyCommit();
}
}
/**
* Notify the transaction manager.
*/
protected void notifyRollback(Throwable cause) {
if (manager != null) {
if (queryOnly) {
manager.notifyOfQueryOnly(this);
} else {
manager.notifyOfRollback(this, cause);
}
}
}
/**
* Rollback the transaction.
*/
@Override
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.
*/
@Override
public void rollback(Throwable cause) throws PersistenceException {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
firePreRollback();
try {
performRollback();
} catch (Exception ex) {
throw new PersistenceException(ex);
} finally {
// these will not throw an exception
firePostRollback();
deactivate();
notifyRollback(cause);
}
}
/**
* If the transaction is active then perform rollback.
*/
@Override
public void end() throws PersistenceException {
if (isActive()) {
rollback();
}
}
/**
* Return true if the transaction is active.
*/
@Override
public boolean isActive() {
return active;
}
@Override
public boolean isPersistCascade() {
return persistCascade;
}
@Override
public void setPersistCascade(boolean persistCascade) {
this.persistCascade = persistCascade;
}
@Override
public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) {
getEvent().add(tableName, inserts, updates, deletes);
}
@Override
public void putUserObject(String name, Object value) {
if (userObjects == null) {
userObjects = new HashMap();
}
userObjects.put(name, value);
}
@Override
public Object getUserObject(String name) {
if (userObjects == null) {
return null;
}
return userObjects.get(name);
}
/**
* Alias for end(), which enables this class to be used in try-with-resources.
*/
@Override
public void close() throws IOException {
try {
end();
} catch (PersistenceException ex) {
throw new IOException(ex);
}
}
}