From 3f2676ab49bbd1e78ae573cb65abf1687b21f65a Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 16 Jul 2014 21:58:32 +1200 Subject: [PATCH] Fix for #168 - ENH: DataSource - add support for configuring autocommit=true --- .../avaje/ebean/config/DataSourceConfig.java | 17 +++ .../com/avaje/ebean/config/ServerConfig.java | 22 +++ .../server/core/InternalConfiguration.java | 37 ++++- .../server/lib/sql/DataSourcePool.java | 2 +- .../AutoCommitJdbcTransaction.java | 27 ++++ .../AutoCommitTransactionManager.java | 36 +++++ .../server/transaction/JdbcTransaction.java | 45 +++--- .../transaction/TransactionManager.java | 140 +++++++++--------- .../transaction/TestAutoCommitDataSource.java | 55 +++++++ src/test/resources/ebean.properties | 7 + 10 files changed, 299 insertions(+), 89 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitTransactionManager.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/transaction/TestAutoCommitDataSource.java diff --git a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java index 5e55e4065..413068165 100644 --- a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java +++ b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java @@ -32,6 +32,8 @@ public class DataSourceConfig { private int isolationLevel = Transaction.READ_COMMITTED; + private boolean autoCommit; + private String heartbeatSql; private int heartbeatFreqSecs = 30; @@ -131,6 +133,20 @@ public class DataSourceConfig { public void setIsolationLevel(int isolationLevel) { this.isolationLevel = isolationLevel; } + + /** + * Return autoCommit setting. + */ + public boolean isAutoCommit() { + return autoCommit; + } + + /** + * Set to true to turn on autoCommit. + */ + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } /** * Return the minimum number of connections the pool should maintain. @@ -450,6 +466,7 @@ public class DataSourceConfig { String dbUrl = properties.get(prefix + "databaseUrl", null); this.url = properties.get(prefix + "url", dbUrl); + this.autoCommit = properties.getBoolean(prefix + "autoCommit", false); this.captureStackTrace = properties.getBoolean(prefix + "captureStackTrace", false); this.maxStackTraceSize = properties.getInt(prefix + "maxStackTraceSize", 5); this.leakTimeMinutes = properties.getInt(prefix + "leakTimeMinutes", 30); diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index c3053c3b0..b49052417 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -175,6 +175,13 @@ public class ServerConfig { */ private DataSourceConfig dataSourceConfig = new DataSourceConfig(); + /** + * Set to true if the DataSource uses autoCommit. + *

+ * Indicates that Ebean should use autoCommit friendly Transactions and TransactionManager. + */ + private boolean autoCommitMode; + /** * The data source JNDI name if using a JNDI DataSource. */ @@ -582,6 +589,20 @@ public class ServerConfig { public void setDataSourceJndiName(String dataSourceJndiName) { this.dataSourceJndiName = dataSourceJndiName; } + + /** + * Return true if autoCommit mode is on. This indicates to Ebean to use autoCommit friendly Transactions and TransactionManager. + */ + public boolean isAutoCommitMode() { + return autoCommitMode; + } + + /** + * Set to true if autoCommit mode is on and Ebean should use autoCommit friendly Transactions and TransactionManager. + */ + public void setAutoCommitMode(boolean autoCommitMode) { + this.autoCommitMode = autoCommitMode; + } /** * Return a value used to represent TRUE in the database. @@ -1281,6 +1302,7 @@ public class ServerConfig { loadDataSourceSettings(p); + autoCommitMode = p.getBoolean("autoCommitMode", false); useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", false); namingConvention = createNamingConvention(p); databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform"); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index 8924def4c..e6f2c404a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -1,5 +1,7 @@ package com.avaje.ebeaninternal.server.core; +import javax.sql.DataSource; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +24,7 @@ import com.avaje.ebeaninternal.server.deploy.parse.DeployCreateProperties; import com.avaje.ebeaninternal.server.deploy.parse.DeployInherit; import com.avaje.ebeaninternal.server.deploy.parse.DeployUtil; import com.avaje.ebeaninternal.server.expression.DefaultExpressionFactory; +import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool; import com.avaje.ebeaninternal.server.persist.Binder; import com.avaje.ebeaninternal.server.persist.DefaultPersister; import com.avaje.ebeaninternal.server.query.CQueryEngine; @@ -31,6 +34,7 @@ import com.avaje.ebeaninternal.server.resource.ResourceManager; import com.avaje.ebeaninternal.server.resource.ResourceManagerFactory; import com.avaje.ebeaninternal.server.text.json.DJsonContext; import com.avaje.ebeaninternal.server.text.json.DefaultJsonValueAdapter; +import com.avaje.ebeaninternal.server.transaction.AutoCommitTransactionManager; import com.avaje.ebeaninternal.server.transaction.DefaultTransactionScopeManager; import com.avaje.ebeaninternal.server.transaction.ExternalTransactionScopeManager; import com.avaje.ebeaninternal.server.transaction.JtaTransactionManager; @@ -42,8 +46,6 @@ import com.avaje.ebeaninternal.server.type.TypeManager; /** * Used to extend the ServerConfig with additional objects used to configure and * construct an EbeanServer. - * - * @author rbygrave */ public class InternalConfiguration { @@ -113,8 +115,7 @@ public class InternalConfiguration { this.beanDescriptorManager = new BeanDescriptorManager(this); beanDescriptorManager.deploy(); - this.transactionManager = new TransactionManager(clusterManager, backgroundExecutor, - serverConfig, beanDescriptorManager, this.getBootupClasses()); + this.transactionManager = createTransactionManager(); this.cQueryEngine = new CQueryEngine(serverConfig.getDatabasePlatform(), binder); @@ -131,6 +132,34 @@ public class InternalConfiguration { } } + + /** + * Create the TransactionManager taking into account autoCommit mode. + */ + private TransactionManager createTransactionManager() { + + if (isAutoCommitMode()) { + return new AutoCommitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses()); + } + + return new TransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses()); + } + + /** + * Return true if autoCommit mode is on. + */ + private boolean isAutoCommitMode() { + if (serverConfig.isAutoCommitMode()) { + // explicitly set + return true; + } + DataSource dataSource = serverConfig.getDataSource(); + if (dataSource instanceof DataSourcePool && ((DataSourcePool)dataSource).getAutoCommit()) { + // We know the DataSourcePool is using autoCommit + return true; + } + return false; + } public JsonContext createJsonContext(SpiEbeanServer server) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java index f6fd4df49..0c1c5c2e4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java @@ -176,7 +176,7 @@ public class DataSourcePool implements DataSource { this.name = name; this.poolListener = createPoolListener(params.getPoolListener()); - this.autoCommit = false; + this.autoCommit = params.isAutoCommit(); this.transactionIsolation = params.getIsolationLevel(); this.maxInactiveMillis = 1000 * params.getMaxInactiveTimeSecs(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java new file mode 100644 index 000000000..52b6c9775 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java @@ -0,0 +1,27 @@ +package com.avaje.ebeaninternal.server.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * AutoCommit friendly Transaction. + *

+ * Skips actual commit and rollback as these are performed automatically. + */ +public class AutoCommitJdbcTransaction extends JdbcTransaction { + + public AutoCommitJdbcTransaction(String id, boolean explicit, Connection connection, TransactionManager manager) { + super(id, explicit, connection, manager); + } + + @Override + protected void performRollback() throws SQLException { + // do nothing as autoCommit + } + + @Override + protected void performCommit() throws SQLException { + // do nothing as autoCommit + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitTransactionManager.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitTransactionManager.java new file mode 100644 index 000000000..ede0fce79 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/AutoCommitTransactionManager.java @@ -0,0 +1,36 @@ +package com.avaje.ebeaninternal.server.transaction; + +import java.sql.Connection; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebeaninternal.api.SpiTransaction; +import com.avaje.ebeaninternal.server.cluster.ClusterManager; +import com.avaje.ebeaninternal.server.core.BootupClasses; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; + +/** + * AutoCommit based TransactionManager. + *

+ * Intended to be used if when autoCommit mode is desired. + */ +public class AutoCommitTransactionManager extends TransactionManager { + + public AutoCommitTransactionManager(ClusterManager clusterManager, BackgroundExecutor backgroundExecutor, + ServerConfig config, BeanDescriptorManager descMgr, BootupClasses bootupClasses) { + + super(clusterManager, backgroundExecutor, config, descMgr, bootupClasses); + } + + /** + * Create an autoCommit based Transaction. + */ + @Override + protected SpiTransaction createTransaction(boolean explicit, Connection c, long id) { + + return new AutoCommitJdbcTransaction(prefix + id, explicit, c, this); + } + + + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java index cbdf37e92..7b1b67a2f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java @@ -540,28 +540,40 @@ public class JdbcTransaction implements SpiTransaction { * rollback or just close the connection for performance. *

*/ - private void connectionEndForQueryOnly() { + protected void connectionEndForQueryOnly() { try { switch (onQueryOnly) { case ROLLBACK: - connection.rollback(); + performRollback(); break; case COMMIT: - connection.commit(); + performCommit(); break; case CLOSE_ON_READCOMMITTED: - // Connection is closed via deactivate() which follows - // This optimisation is only available at READ COMMITTED Isolation + // valid at READ COMMITTED Isolation break; default: - connection.rollback(); + performRollback(); } } catch (SQLException e) { - String m = "Error when ending a query only transaction via " + onQueryOnly; - logger.error(m, 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(); + } + /** * End the transaction on a query only request. */ @@ -594,7 +606,7 @@ public class JdbcTransaction implements SpiTransaction { if (batchControl != null && !batchControl.isEmpty()) { batchControl.flush(); } - connection.commit(); + performCommit(); } } catch (Exception e) { @@ -611,13 +623,12 @@ public class JdbcTransaction implements SpiTransaction { * 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); + if (manager != null) { + if (queryOnly) { + manager.notifyOfQueryOnly(false, this, cause); + } else { + manager.notifyOfRollback(this, cause); + } } } @@ -637,7 +648,7 @@ public class JdbcTransaction implements SpiTransaction { throw new IllegalStateException(illegalStateMessage); } try { - connection.rollback(); + performRollback(); } catch (Exception ex) { throw new PersistenceException(ex); diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java index 58460c3f1..1c267ee74 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java @@ -42,7 +42,7 @@ public class TransactionManager { public static final Logger TXN_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.TXN"); /** - * The behaviour desired when ending a query only transaction. + * The behavior desired when ending a query only transaction. */ public enum OnQueryOnly { @@ -62,47 +62,47 @@ public class TransactionManager { COMMIT } - private final BeanDescriptorManager beanDescriptorManager; + protected final BeanDescriptorManager beanDescriptorManager; /** * Prefix for transaction id's (logging). */ - private final String prefix; + protected final String prefix; - private final String externalTransPrefix; + protected final String externalTransPrefix; /** * The dataSource of connections. */ - private final DataSource dataSource; + protected final DataSource dataSource; /** * Flag to indicate the default Isolation is READ COMMITTED. This enables us * to close queryOnly transactions rather than commit or rollback them. */ - private final OnQueryOnly onQueryOnly; + protected final OnQueryOnly onQueryOnly; /** * The default batchMode for transactions. */ - private final boolean defaultBatchMode; + protected final boolean defaultBatchMode; - private final BackgroundExecutor backgroundExecutor; + protected final BackgroundExecutor backgroundExecutor; - private final ClusterManager clusterManager; + protected final ClusterManager clusterManager; - private final String serverName; + protected final String serverName; /** * Id's for transaction logging. */ - private AtomicLong transactionCounter = new AtomicLong(1000); + protected AtomicLong transactionCounter = new AtomicLong(1000); - private int clusterDebugLevel; + protected int clusterDebugLevel; - private final BulkEventListenerMap bulkEventListenerMap; + protected final BulkEventListenerMap bulkEventListenerMap; - private TransactionEventListener[] transactionEventListeners; + protected TransactionEventListener[] transactionEventListeners; /** * Create the TransactionManager @@ -146,14 +146,14 @@ public class TransactionManager { ((DataSourcePool)dataSource).shutdown(deregisterDriver); } } - - public BeanDescriptorManager getBeanDescriptorManager() { - return beanDescriptorManager; - } - public BulkEventListenerMap getBulkEventListenerMap() { - return bulkEventListenerMap; - } + public BeanDescriptorManager getBeanDescriptorManager() { + return beanDescriptorManager; + } + + public BulkEventListenerMap getBulkEventListenerMap() { + return bulkEventListenerMap; + } /** * Return the behaviour to use when a query only transaction is committed. @@ -219,26 +219,26 @@ public class TransactionManager { return dataSource; } - /** - * Return the cluster debug level. - */ - public int getClusterDebugLevel() { - return clusterDebugLevel; - } + /** + * Return the cluster debug level. + */ + public int getClusterDebugLevel() { + return clusterDebugLevel; + } - /** - * Set the cluster debug level. - */ - public void setClusterDebugLevel(int clusterDebugLevel) { - this.clusterDebugLevel = clusterDebugLevel; - } + /** + * Set the cluster debug level. + */ + public void setClusterDebugLevel(int clusterDebugLevel) { + this.clusterDebugLevel = clusterDebugLevel; + } - /** - * Defines the type of behaviour to use when closing a transaction that was used to query data only. - */ - public OnQueryOnly getOnQueryOnly() { - return onQueryOnly; - } + /** + * Defines the type of behavior to use when closing a transaction that was used to query data only. + */ + public OnQueryOnly getOnQueryOnly() { + return onQueryOnly; + } /** * Wrap the externally supplied Connection. @@ -273,7 +273,7 @@ public class TransactionManager { c = dataSource.getConnection(); long id = transactionCounter.incrementAndGet(); - JdbcTransaction t = new JdbcTransaction(prefix + id, explicit, c, this); + SpiTransaction t = createTransaction(explicit, c, id); // set the default batch mode. This can be on for // jdbc drivers that support getGeneratedKeys @@ -309,7 +309,7 @@ public class TransactionManager { c = dataSource.getConnection(); long id = transactionCounter.incrementAndGet(); - JdbcTransaction t = new JdbcTransaction(prefix + id, false, c, this); + SpiTransaction t = createTransaction(false, c, id); // set the default batch mode. Can be true for // jdbc drivers that support getGeneratedKeys @@ -336,6 +336,13 @@ public class TransactionManager { } } + /** + * Create a new transaction. + */ + protected SpiTransaction createTransaction(boolean explicit, Connection c, long id) { + return new JdbcTransaction(prefix + id, explicit, c, this); + } + /** * Process a local rolled back transaction. */ @@ -446,34 +453,33 @@ public class TransactionManager { backgroundExecutor.execute(postCommit.notifyPersistListeners()); } - - - /** - * Notify local BeanPersistListeners etc of events from another server in the cluster. - */ - public void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { - - if (clusterDebugLevel > 0 || logger.isDebugEnabled()){ - logger.info("Cluster Received: "+remoteEvent.toString()); - } - List tableIUDList = remoteEvent.getTableIUDList(); - if (tableIUDList != null){ - for (int i = 0; i < tableIUDList.size(); i++) { - TableIUD tableIUD = tableIUDList.get(i); - beanDescriptorManager.cacheNotify(tableIUD); - } - } - - List beanPersistList = remoteEvent.getBeanPersistList(); - if (beanPersistList != null){ - for (int i = 0; i < beanPersistList.size(); i++) { - BeanPersistIds beanPersist = beanPersistList.get(i); - beanPersist.notifyCacheAndListener(); - } - } - + /** + * Notify local BeanPersistListeners etc of events from another server in the cluster. + */ + public void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { + + if (clusterDebugLevel > 0 || logger.isDebugEnabled()) { + logger.info("Cluster Received: " + remoteEvent.toString()); } + + List tableIUDList = remoteEvent.getTableIUDList(); + if (tableIUDList != null) { + for (int i = 0; i < tableIUDList.size(); i++) { + TableIUD tableIUD = tableIUDList.get(i); + beanDescriptorManager.cacheNotify(tableIUD); + } + } + + List beanPersistList = remoteEvent.getBeanPersistList(); + if (beanPersistList != null) { + for (int i = 0; i < beanPersistList.size(); i++) { + BeanPersistIds beanPersist = beanPersistList.get(i); + beanPersist.notifyCacheAndListener(); + } + } + + } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/transaction/TestAutoCommitDataSource.java b/src/test/java/com/avaje/ebeaninternal/server/transaction/TestAutoCommitDataSource.java new file mode 100644 index 000000000..a7614d5cc --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/transaction/TestAutoCommitDataSource.java @@ -0,0 +1,55 @@ +package com.avaje.ebeaninternal.server.transaction; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.EbeanServerFactory; +import com.avaje.ebean.Transaction; +import com.avaje.ebean.config.GlobalProperties; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.tests.model.basic.UTDetail; + +public class TestAutoCommitDataSource extends BaseTestCase { + + @Test + public void test() { + + ServerConfig config = new ServerConfig(); + config.setName("h2autocommit"); + config.loadFromProperties(); + + config.addClass(UTDetail.class); + config.setDdlGenerate(true); + config.setDdlRun(true); + config.setAutoCommitMode(true); + + GlobalProperties.setSkipPrimaryServer(true); + + EbeanServer ebeanServer = EbeanServerFactory.create(config); + + + UTDetail detail1 = new UTDetail("one", 12, 30D); + UTDetail detail2 = new UTDetail("two", 11, 30D); + UTDetail detail3 = new UTDetail("three", 8, 30D); + + Transaction txn = ebeanServer.beginTransaction(); + try { + txn.setBatchMode(true); + ebeanServer.save(detail1); + ebeanServer.save(detail2); + ebeanServer.save(detail3); + txn.commit(); + + } finally { + txn.end(); + } + + List details = ebeanServer.find(UTDetail.class).findList(); + Assert.assertEquals(3, details.size()); + + } +} diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index 16fda2bb2..9849f9cdd 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -85,6 +85,13 @@ datasource.h2.poolListener=com.avaje.tests.basic.MyTestDataSourcePoolListener datasource.h2.customProperties=IGNORECASE=TRUE;MODE=Oracle; +datasource.h2autocommit.autoCommit=true +datasource.h2autocommit.username=sa +datasource.h2autocommit.password= +datasource.h2autocommit.databaseUrl=jdbc:h2:mem:autocommittests +datasource.h2autocommit.databaseDriver=org.h2.Driver + + datasource.sqlite.username= datasource.sqlite.password= datasource.sqlite.databaseUrl=jdbc:sqlite:mydb.db