#1232 - Performance: Add support for a second read-only DataSource (for implicit query-only transactions)

This commit is contained in:
Rob Bygrave
2018-01-09 16:35:22 +13:00
parent 01539091ef
commit 932986a6b5
27 changed files with 1128 additions and 100 deletions
@@ -273,11 +273,29 @@ public class ServerConfig {
*/
private DataSource dataSource;
/**
* The read only data source (can be null).
*/
private DataSource readOnlyDataSource;
/**
* The data source config.
*/
private DataSourceConfig dataSourceConfig = new DataSourceConfig();
/**
* When true create a read only DataSource using readOnlyDataSourceConfig defaulting values from dataSourceConfig.
* I believe this will default to true in some future release (as it has a nice performance benefit).
*
* autoReadOnlyDataSource is an unfortunate name for this config option but I haven't come up with a better one.
*/
private boolean autoReadOnlyDataSource;
/**
* Optional configuration for a read only data source.
*/
private DataSourceConfig readOnlyDataSourceConfig = new DataSourceConfig();
/**
* The db migration config (migration resource path etc).
*/
@@ -1442,6 +1460,28 @@ public class ServerConfig {
this.dataSource = dataSource;
}
/**
* Return the read only DataSource.
*/
public DataSource getReadOnlyDataSource() {
return readOnlyDataSource;
}
/**
* Set the read only DataSource.
* <p>
* Note that the DataSource is expected to use AutoCommit true mode avoiding the need
* for explicit commit (or rollback).
* </p>
* <p>
* This read only DataSource will be used for implicit query only transactions. It is not
* used if the transaction is created explicitly or if the query is an update or delete query.
* </p>
*/
public void setReadOnlyDataSource(DataSource readOnlyDataSource) {
this.readOnlyDataSource = readOnlyDataSource;
}
/**
* Return the configuration to build a DataSource using Ebean's own DataSource
* implementation.
@@ -1458,6 +1498,42 @@ public class ServerConfig {
this.dataSourceConfig = dataSourceConfig;
}
/**
* Return true if Ebean should create a DataSource for use with implicit read only transactions.
*/
public boolean isAutoReadOnlyDataSource() {
return autoReadOnlyDataSource;
}
/**
* Set to true if Ebean should create a DataSource for use with implicit read only transactions.
*/
public void setAutoReadOnlyDataSource(boolean autoReadOnlyDataSource) {
this.autoReadOnlyDataSource = autoReadOnlyDataSource;
}
/**
* Return the configuration for the read only DataSource.
* <p>
* This is only used if autoReadOnlyDataSource is true.
* </p>
* <p>
* The driver, url, username and password default to the configuration for the main DataSource if they are not
* set on this configuration. This means there is actually no need to set any configuration here and we only
* set configuration for url, username and password etc if it is different from the main DataSource.
* </p>
*/
public DataSourceConfig getReadOnlyDataSourceConfig() {
return readOnlyDataSourceConfig;
}
/**
* Set the configuration for the read only DataSource.
*/
public void setReadOnlyDataSourceConfig(DataSourceConfig readOnlyDataSourceConfig) {
this.readOnlyDataSourceConfig = readOnlyDataSourceConfig;
}
/**
* Return the JNDI name of the DataSource to use.
*/
@@ -2607,6 +2683,7 @@ public class ServerConfig {
explicitTransactionBeginMode = p.getBoolean("explicitTransactionBeginMode", explicitTransactionBeginMode);
autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode);
useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", useJtaTransactionManager);
autoReadOnlyDataSource = p.getBoolean("autoReadOnlyDataSource", autoReadOnlyDataSource);
backgroundExecutorSchedulePoolSize = p.getInt("backgroundExecutorSchedulePoolSize", backgroundExecutorSchedulePoolSize);
backgroundExecutorShutdownSecs = p.getInt("backgroundExecutorShutdownSecs", backgroundExecutorShutdownSecs);
@@ -47,4 +47,9 @@ public interface SpiServer extends EbeanServer {
*/
DataSource getDataSource();
/**
* Return the associated read only DataSource for this EbeanServer instance (can be null).
*/
DataSource getReadOnlyDataSource();
}
@@ -98,17 +98,30 @@ public interface SpiQuery<T> extends Query<T>, TxnProfileEventCodes {
/**
* Delete query.
*/
DELETE(FIND_DELETE),
DELETE(FIND_DELETE, true),
/**
* Update query.
*/
UPDATE(FIND_UPDATE);
UPDATE(FIND_UPDATE, true);
boolean update;
String profileEventId;
Type(String profileEventId) {
this(profileEventId, false);
}
Type(String profileEventId, boolean update) {
this.profileEventId = profileEventId;
this.update = update;
}
/**
* Return true if this is an Update or Delete query (not read only).
*/
public boolean isUpdate() {
return update;
}
public String profileEventId() {
@@ -278,18 +278,21 @@ public class DefaultContainer implements SpiContainer {
*/
private void setDataSource(ServerConfig config) {
if (config.getDataSource() == null) {
config.setDataSource(getDataSourceFromConfig(config));
config.setDataSource(getDataSourceFromConfig(config, false));
}
if (config.getReadOnlyDataSource() == null && config.isAutoReadOnlyDataSource()) {
config.setReadOnlyDataSource(getDataSourceFromConfig(config, true));
}
}
private DataSource getDataSourceFromConfig(ServerConfig config) {
private DataSource getDataSourceFromConfig(ServerConfig config, boolean readOnly) {
if (isOfflineMode(config)) {
logger.debug("... DbOffline using platform [{}]", DbOffline.getPlatform());
return null;
}
if (config.getDataSourceJndiName() != null) {
if (!readOnly && config.getDataSourceJndiName() != null) {
DataSource ds = jndiDataSourceFactory.lookup(config.getDataSourceJndiName());
if (ds == null) {
throw new PersistenceException("JNDI lookup for DataSource " + config.getDataSourceJndiName() + " returned null.");
@@ -298,7 +301,7 @@ public class DefaultContainer implements SpiContainer {
}
}
DataSourceConfig dsConfig = config.getDataSourceConfig();
DataSourceConfig dsConfig = (readOnly) ? config.getReadOnlyDataSourceConfig() : config.getDataSourceConfig();
if (dsConfig == null) {
throw new PersistenceException("No DataSourceConfig defined for " + config.getName());
}
@@ -323,7 +326,14 @@ public class DefaultContainer implements SpiContainer {
attachListener(config, dsConfig);
return factory.createPool(config.getName(), dsConfig);
if (readOnly) {
// setup to use AutoCommit such that we skip explicit commit
dsConfig.setAutoCommit(true);
//dsConfig.setReadOnly(true);
dsConfig.setDefaults(config.getDataSourceConfig());
}
String poolName = config.getName() + (readOnly ? "-ro" : "");
return factory.createPool(poolName, dsConfig);
}
/**
@@ -384,10 +384,10 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return transactionManager.getDataSource();
}
// @Override
// public DataSource getReadOnlyDataSource() {
// return transactionManager.getReadOnlyDataSource();
// }
@Override
public DataSource getReadOnlyDataSource() {
return transactionManager.getReadOnlyDataSource();
}
@Override
public ReadAuditPrepare getReadAuditPrepare() {
@@ -410,11 +410,11 @@ public class InternalConfiguration {
case DB:
return new MultiTenantDbSupplier(serverConfig.getCurrentTenantProvider(), serverConfig.getTenantDataSourceProvider());
case SCHEMA:
return new MultiTenantDbSchemaSupplier(serverConfig.getCurrentTenantProvider(), serverConfig.getDataSource(), serverConfig.getTenantSchemaProvider());
return new MultiTenantDbSchemaSupplier(serverConfig.getCurrentTenantProvider(), serverConfig.getDataSource(), serverConfig.getReadOnlyDataSource(), serverConfig.getTenantSchemaProvider());
case CATALOG:
return new MultiTenantDbCatalogSupplier(serverConfig.getCurrentTenantProvider(), serverConfig.getDataSource(), serverConfig.getTenantCatalogProvider());
return new MultiTenantDbCatalogSupplier(serverConfig.getCurrentTenantProvider(), serverConfig.getDataSource(), serverConfig.getReadOnlyDataSource(), serverConfig.getTenantCatalogProvider());
default:
return new SimpleDataSourceProvider(serverConfig.getDataSource());
return new SimpleDataSourceProvider(serverConfig.getDataSource(), serverConfig.getReadOnlyDataSource());
}
}
@@ -17,19 +17,21 @@ import java.util.logging.Logger;
*/
public class MultiTenantDbCatalogSupplier implements DataSourceSupplier {
private final CurrentTenantProvider tenantProvider;
private final DataSource dataSource;
private final TenantCatalogProvider catalogProvider;
private final DataSource readOnlyDataSource;
private final CatalogDataSource catalogDataSource;
private final CatalogDataSource readOnly;
MultiTenantDbCatalogSupplier(CurrentTenantProvider tenantProvider, DataSource dataSource, TenantCatalogProvider catalogProvider) {
this.tenantProvider = tenantProvider;
MultiTenantDbCatalogSupplier(CurrentTenantProvider tenantProvider, DataSource dataSource, DataSource readOnlyDataSource, TenantCatalogProvider catalogProvider) {
this.dataSource = dataSource;
this.catalogProvider = catalogProvider;
this.catalogDataSource = new CatalogDataSource();
this.readOnlyDataSource = readOnlyDataSource;
this.catalogDataSource = new CatalogDataSource(dataSource, tenantProvider, catalogProvider);
if (readOnlyDataSource == null) {
this.readOnly = null;
} else {
this.readOnly = new CatalogDataSource(readOnlyDataSource, tenantProvider, catalogProvider);
}
}
@Override
@@ -37,28 +39,48 @@ public class MultiTenantDbCatalogSupplier implements DataSourceSupplier {
return catalogDataSource;
}
@Override
public DataSource getReadOnlyDataSource() {
return readOnly;
}
@Override
public Connection getConnection(Object tenantId) throws SQLException {
return catalogDataSource.getConnectionForTenant(tenantId);
}
@Override
public Connection getReadOnlyConnection(Object tenantId) throws SQLException {
return readOnly.getConnectionForTenant(tenantId);
}
@Override
public void shutdown(boolean deregisterDriver) {
if (readOnlyDataSource instanceof DataSourcePool) {
((DataSourcePool) readOnlyDataSource).shutdown(false);
}
if (dataSource instanceof DataSourcePool) {
((DataSourcePool) dataSource).shutdown(deregisterDriver);
}
}
/**
* Returns the DB catalog for the current user Tenant Id.
*/
private String tenantCatalog() {
return catalogProvider.catalog(tenantProvider.currentId());
}
private class CatalogDataSource implements DataSource {
CatalogDataSource() {
final DataSource dataSource;
final CurrentTenantProvider tenantProvider;
final TenantCatalogProvider catalogProvider;
CatalogDataSource(DataSource dataSource, CurrentTenantProvider tenantProvider, TenantCatalogProvider catalogProvider) {
this.dataSource = dataSource;
this.tenantProvider = tenantProvider;
this.catalogProvider = catalogProvider;
}
/**
* Returns the DB catalog for the current user Tenant Id.
*/
private String tenantCatalog() {
return catalogProvider.catalog(tenantProvider.currentId());
}
/**
@@ -17,19 +17,21 @@ import java.util.logging.Logger;
*/
class MultiTenantDbSchemaSupplier implements DataSourceSupplier {
private final CurrentTenantProvider tenantProvider;
private final DataSource dataSource;
private final TenantSchemaProvider schemaProvider;
private final DataSource readOnlyDataSource;
private final SchemaDataSource schemaDataSource;
private final SchemaDataSource readOnly;
MultiTenantDbSchemaSupplier(CurrentTenantProvider tenantProvider, DataSource dataSource, TenantSchemaProvider schemaProvider) {
this.tenantProvider = tenantProvider;
MultiTenantDbSchemaSupplier(CurrentTenantProvider tenantProvider, DataSource dataSource, DataSource readOnlyDataSource, TenantSchemaProvider schemaProvider) {
this.dataSource = dataSource;
this.schemaProvider = schemaProvider;
this.schemaDataSource = new SchemaDataSource();
this.readOnlyDataSource = readOnlyDataSource;
this.schemaDataSource = new SchemaDataSource(dataSource, schemaProvider, tenantProvider);
if (readOnlyDataSource == null) {
this.readOnly = null;
} else {
this.readOnly = new SchemaDataSource(readOnlyDataSource, schemaProvider, tenantProvider);
}
}
@Override
@@ -37,28 +39,44 @@ class MultiTenantDbSchemaSupplier implements DataSourceSupplier {
return schemaDataSource;
}
@Override
public DataSource getReadOnlyDataSource() {
return readOnly;
}
@Override
public Connection getConnection(Object tenantId) throws SQLException {
return schemaDataSource.getConnectionForTenant(tenantId);
}
@Override
public Connection getReadOnlyConnection(Object tenantId) throws SQLException {
return readOnly.getConnectionForTenant(tenantId);
}
@Override
public void shutdown(boolean deregisterDriver) {
if (readOnlyDataSource instanceof DataSourcePool) {
((DataSourcePool) readOnlyDataSource).shutdown(false);
}
if (dataSource instanceof DataSourcePool) {
((DataSourcePool) dataSource).shutdown(deregisterDriver);
}
}
/**
* Returns the DB schema for the current user Tenant Id.
* Tenant schema aware DataSource.
*/
private String tenantSchema() {
return schemaProvider.schema(tenantProvider.currentId());
}
private static class SchemaDataSource implements DataSource {
private class SchemaDataSource implements DataSource {
private final DataSource dataSource;
private final TenantSchemaProvider schemaProvider;
private final CurrentTenantProvider tenantProvider;
SchemaDataSource() {
SchemaDataSource(DataSource dataSource, TenantSchemaProvider schemaProvider, CurrentTenantProvider tenantProvider) {
this.dataSource = dataSource;
this.schemaProvider = schemaProvider;
this.tenantProvider = tenantProvider;
}
/**
@@ -70,6 +88,13 @@ class MultiTenantDbSchemaSupplier implements DataSourceSupplier {
return connection;
}
/**
* Returns the DB schema for the current user Tenant Id.
*/
private String tenantSchema() {
return schemaProvider.schema(tenantProvider.currentId());
}
/**
* Return the connection with the appropriate DB schema set.
*/
@@ -22,6 +22,12 @@ class MultiTenantDbSupplier implements DataSourceSupplier {
this.dataSourceProvider = dataSourceProvider;
}
@Override
public DataSource getReadOnlyDataSource() {
// read only datasource not supported with DB per tenant at this stage
return null;
}
@Override
public DataSource getDataSource() {
return dataSourceProvider.dataSource(tenantProvider.currentId());
@@ -32,6 +38,11 @@ class MultiTenantDbSupplier implements DataSourceSupplier {
return dataSourceProvider.dataSource(tenantId).getConnection();
}
@Override
public Connection getReadOnlyConnection(Object tenantId) throws SQLException {
throw new SQLException("Not currently supported");
}
@Override
public void shutdown(boolean deregisterDriver) {
dataSourceProvider.shutdown(deregisterDriver);
@@ -244,8 +244,14 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
// maybe a current one
transaction = ebeanServer.currentServerTransaction();
if (transaction == null) {
// create an implicit transaction to execute this query
transaction = ebeanServer.createQueryTransaction(query.getTenantId());
if (query.getType().isUpdate()) {
// bulk update or delete query
transaction = ebeanServer.beginServerTransaction();
} else {
// create an implicit transaction to execute this query
// potentially using read-only DataSource with autoCommit
transaction = ebeanServer.createQueryTransaction(query.getTenantId());
}
createdTransaction = true;
}
}
@@ -14,8 +14,11 @@ class SimpleDataSourceProvider implements DataSourceSupplier {
private final DataSource dataSource;
SimpleDataSourceProvider(DataSource dataSource) {
private final DataSource readOnlyDataSource;
SimpleDataSourceProvider(DataSource dataSource, DataSource readOnlyDataSource) {
this.dataSource = dataSource;
this.readOnlyDataSource = readOnlyDataSource;
}
@Override
@@ -23,13 +26,26 @@ class SimpleDataSourceProvider implements DataSourceSupplier {
return dataSource;
}
@Override
public DataSource getReadOnlyDataSource() {
return readOnlyDataSource;
}
@Override
public Connection getConnection(Object tenantId) throws SQLException {
return dataSource.getConnection();
}
@Override
public Connection getReadOnlyConnection(Object tenantId) throws SQLException {
return readOnlyDataSource.getConnection();
}
@Override
public void shutdown(boolean deregisterDriver) {
if (readOnlyDataSource instanceof DataSourcePool){
((DataSourcePool) readOnlyDataSource).shutdown(false);
}
if (dataSource instanceof DataSourcePool){
((DataSourcePool) dataSource).shutdown(deregisterDriver);
}
@@ -31,6 +31,8 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
private int paramIndex;
private final boolean enableBindLog;
private StringBuilder bindLog;
public DefaultExpressionRequest(SpiOrmQueryRequest<?> queryRequest, DeployParser deployParser, Binder binder, SpiExpressionList<?> expressionList) {
@@ -39,6 +41,7 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
this.deployParser = deployParser;
this.binder = binder;
this.expressionList = expressionList;
this.enableBindLog = binder.isEnableBindLog();
// immediately build the list of bind values (callback style)
expressionList.addBindValues(this);
}
@@ -49,6 +52,7 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
this.deployParser = null;
this.binder = null;
this.expressionList = null;
this.enableBindLog = true;
}
/**
@@ -138,12 +142,14 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
}
private void bindLog(Object val) {
if (bindLog == null) {
bindLog = new StringBuilder();
} else {
bindLog.append(",");
if (enableBindLog) {
if (bindLog == null) {
bindLog = new StringBuilder();
} else {
bindLog.append(",");
}
bindLog.append(val);
}
bindLog.append(val);
}
public String getBindLog() {
@@ -6,6 +6,7 @@ import io.ebeaninternal.server.core.DbExpressionHandler;
import io.ebeaninternal.server.core.Message;
import io.ebeaninternal.server.core.timezone.DataTimeZone;
import io.ebeaninternal.server.persist.platform.MultiValueBind;
import io.ebeaninternal.server.transaction.TransactionManager;
import io.ebeaninternal.server.type.DataBind;
import io.ebeaninternal.server.type.ScalarType;
import io.ebeaninternal.server.type.TypeManager;
@@ -42,6 +43,8 @@ public class Binder {
private final MultiValueBind multiValueBind;
private final boolean enableBindLog;
/**
* Set the PreparedStatement with which to bind variables to.
*/
@@ -54,6 +57,19 @@ public class Binder {
this.dbExpressionHandler = dbExpressionHandler;
this.dataTimeZone = dataTimeZone;
this.multiValueBind = multiValueBind;
this.enableBindLog = enableBindLog();
}
private boolean enableBindLog() {
return TransactionManager.SQL_LOGGER.isDebugEnabled()
|| TransactionManager.SUM_LOGGER.isDebugEnabled();
}
/**
* Return true if bind log is enabled.
*/
public boolean isEnableBindLog() {
return enableBindLog;
}
/**
@@ -1,7 +1,6 @@
package io.ebeaninternal.server.transaction;
import java.sql.Connection;
import java.sql.SQLException;
/**
* AutoCommit friendly Transaction.
@@ -15,17 +14,17 @@ public class AutoCommitJdbcTransaction extends JdbcTransaction {
}
@Override
protected void checkAutoCommit(Connection connection) throws SQLException {
protected void checkAutoCommit(Connection connection) {
// do nothing as autoCommit
}
@Override
protected void performRollback() throws SQLException {
protected void performRollback() {
// do nothing as autoCommit
}
@Override
protected void performCommit() throws SQLException {
protected void performCommit() {
// do nothing as autoCommit
}
@@ -20,6 +20,15 @@ public interface DataSourceSupplier {
*/
DataSource getDataSource();
/**
* Return the read only DataSource to use for the current request.
* <p>
* This can return null meaning that no read only DataSource (with autoCommit)
* is available for use so normal transactions with explicit commit should be used.
* </p>
*/
DataSource getReadOnlyDataSource();
/**
* Return a connection from the DataSource taking into account a tenantId for multi-tenant lazy loading.
*
@@ -28,6 +37,14 @@ public interface DataSourceSupplier {
*/
Connection getConnection(Object tenantId) throws SQLException;
/**
* Return a connection from the read only DataSource taking into account a tenantId for multi-tenant lazy loading.
*
* @param tenantId Most often null but well supplied indicates a multi-tenant lazy loading query
* @return the connection to use
*/
Connection getReadOnlyConnection(Object tenantId) throws SQLException;
/**
* Shutdown the datasource de-registering the JDBC driver if requested.
*/
@@ -0,0 +1,591 @@
package io.ebeaninternal.server.transaction;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.annotation.PersistBatch;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.api.TxnProfileEventCodes;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
import io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeanservice.docstore.api.DocStoreTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* Read only transaction expected to use autoCommit connection and for implicit use only.
* <p>
* This transaction is created implicitly and not expected to be exposed to application code and has
* none of the features for supporting inserts, updates and deletes etc (and throws errors if those
* persisting features are attempted to be used - which is not expected).
* </p>
*/
class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCodes {
private static final Logger logger = LoggerFactory.getLogger(ImplicitReadOnlyTransaction.class);
private static final String illegalStateMessage = "Transaction is Inactive";
private static final String notExpectedMessage = "Not expected on read only transaction";
/**
* The status of the transaction.
*/
protected boolean active;
/**
* The underlying Connection which is expected to use autoCommit such that we avoid the
* explicit commit call at the end of the 'transaction' (for performance).
*/
protected Connection connection;
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
protected PersistenceContext persistenceContext;
private Object tenantId;
private Map<String, Object> userObjects;
/**
* Create without a tenantId.
*/
ImplicitReadOnlyTransaction(Connection connection) {
this.active = true;
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
}
/**
* Create with a tenantId.
*/
ImplicitReadOnlyTransaction(Connection connection, Object tenantId) {
this(connection);
this.tenantId = tenantId;
}
@Override
public long profileOffset() {
return 0;
}
@Override
public void profileEvent(SpiProfileTransactionEvent event) {
// do nothing
}
@Override
public ProfileStream profileStream() {
return null;
}
@Override
public boolean isSkipCache() {
return false;
}
@Override
public void setSkipCache(boolean skipCache) {
}
@Override
public String getLogPrefix() {
return null;
}
@Override
public void addBeanChange(BeanChange beanChange) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void sendChangeLog(ChangeSet changesRequest) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void register(TransactionCallback callback) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public int getDocStoreBatchSize() {
return 0;
}
@Override
public void setDocStoreBatchSize(int docStoreBatchSize) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public DocStoreMode getDocStoreMode() {
return null;
}
@Override
public void setDocStoreMode(DocStoreMode docStoreMode) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void registerDeferred(PersistDeferredRelationship derived) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void registerDeleteBean(Integer persistingBean) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void unregisterDeleteBean(Integer persistedBean) {
throw new IllegalStateException(notExpectedMessage);
}
/**
* Return true if this is a bean that has already been saved/deleted.
*/
@Override
public boolean isRegisteredDeleteBean(Integer persistingBean) {
return false;
}
@Override
public void unregisterBean(Object bean) {
throw new IllegalStateException(notExpectedMessage);
}
/**
* 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) {
return false;
}
@Override
public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void depth(int diff) {
}
/**
* Return the current depth.
*/
@Override
public int depth() {
return 0;
}
@Override
public void markNotQueryOnly() {
}
@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 {
connection.setReadOnly(readOnly);
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
@Override
public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) {
}
@Override
public Boolean isUpdateAllLoadedProperties() {
return null;
}
@Override
public void setBatchMode(boolean batchMode) {
}
@Override
public void setBatch(PersistBatch batchMode) {
}
@Override
public PersistBatch getBatch() {
return null;
}
@Override
public void setBatchOnCascade(PersistBatch batchOnCascadeMode) {
}
@Override
public PersistBatch getBatchOnCascade() {
return null;
}
@Override
public Boolean getBatchGetGeneratedKeys() {
return null;
}
@Override
public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) {
}
@Override
public void setBatchFlushOnMixed(boolean batchFlushOnMixed) {
}
/**
* Return the batchSize specifically set for this transaction or 0.
* <p>
* Returning 0 implies to use the system wide default batch size.
* </p>
*/
@Override
public int getBatchSize() {
return 0;
}
@Override
public void setBatchSize(int batchSize) {
}
@Override
public boolean isBatchFlushOnQuery() {
return false;
}
@Override
public void setBatchFlushOnQuery(boolean 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) {
return false;
}
@Override
public void checkBatchEscalationOnCollection() {
}
@Override
public void flushBatchOnCollection() {
}
@Override
public PersistenceException translate(String message, SQLException cause) {
return new PersistenceException(message, cause);
}
/**
* Flush after completing persist cascade.
*/
@Override
public void flushBatchOnCascade() {
}
@Override
public void flushBatchOnRollback() {
}
@Override
public boolean checkBatchEscalationOnCascade(PersistRequestBean<?> request) {
return false;
}
@Override
public BatchControl getBatchControl() {
return null;
}
/**
* Set the BatchControl to the transaction. This is done once per transaction
* on the first persist request.
*/
@Override
public void setBatchControl(BatchControl batchControl) {
}
/**
* Flush any queued persist requests.
* <p>
* This is general will result in a number of batched PreparedStatements
* executing.
* </p>
*/
@Override
public void flush() {
}
@Override
public void flushBatch() {
flush();
}
/**
* Return the persistence context associated with this transaction.
*/
@Override
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
/**
* Set the persistence context to this transaction.
* <p>
* 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.
* </p>
*/
@Override
public void setPersistenceContext(PersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.persistenceContext = context;
}
@Override
public TransactionEvent getEvent() {
throw new IllegalStateException(notExpectedMessage);
}
/**
* Return true if this was an explicitly created transaction.
*/
@Override
public boolean isExplicit() {
return false;
}
@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(msg);
}
@Override
public void logSummary(String msg) {
TransactionManager.SUM_LOGGER.debug(msg);
}
/**
* Return the transaction id.
*/
@Override
public String getId() {
return null;
}
@Override
public void setTenantId(Object tenantId) {
this.tenantId = tenantId;
}
@Override
public Object getTenantId() {
return tenantId;
}
/**
* 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() {
return getInternalConnection();
}
private void deactivate() {
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;
}
/**
* Perform a commit, fire callbacks and notify l2 cache etc.
* <p>
* This leaves the transaction active and expects another commit
* to occur later (which closes the underlying connection etc).
* </p>
*/
@Override
public void commitAndContinue() {
// do nothing, expect AutoCommit
}
/**
* Commit the transaction.
*/
@Override
public void commit() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
// expect AutoCommit so just deactivate / put back into pool
deactivate();
}
/**
* Return true if the transaction is marked as rollback only.
*/
@Override
public boolean isRollbackOnly() {
return false;
}
/**
* Mark the transaction as rollback only.
*/
@Override
public void setRollbackOnly() {
// expect AutoCommit so we can't really support rollbackOnly
throw new IllegalStateException(notExpectedMessage);
}
/**
* 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);
}
// expect AutoCommit so it really has already committed
deactivate();
}
/**
* 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 false;
}
@Override
public void setPersistCascade(boolean persistCascade) {
}
@Override
public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public DocStoreTransaction getDocStoreTransaction() {
throw new IllegalStateException(notExpectedMessage);
}
@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() {
end();
}
}
@@ -13,37 +13,49 @@ import java.sql.SQLException;
*/
class TransactionFactoryBasic extends TransactionFactory {
final DataSourceSupplier dataSourceSupplier;
private final DataSource dataSource;
TransactionFactoryBasic(TransactionManager manager, DataSourceSupplier dataSourceSupplier) {
super(manager);
this.dataSourceSupplier = dataSourceSupplier;
this.dataSource = dataSourceSupplier.getDataSource();
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
return create(0,false);
}
@Override
public SpiTransaction createTransaction(int profileId, boolean explicit, int isolationLevel) {
SpiTransaction t = create(profileId, explicit);
return setIsolationLevel(t, explicit, isolationLevel);
}
private SpiTransaction create(int profileId, boolean explicit) {
Connection c = null;
Connection connection = null;
try {
c = dataSource.getConnection();
return manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
connection = dataSource.getConnection();
return create(0, false, connection);
} catch (PersistenceException ex) {
JdbcClose.close(c);
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
@Override
public SpiTransaction createTransaction(int profileId, boolean explicit, int isolationLevel) {
Connection connection = null;
try {
connection = dataSource.getConnection();
SpiTransaction t = create(profileId, explicit, connection);
return setIsolationLevel(t, explicit, isolationLevel);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
private SpiTransaction create(int profileId, boolean explicit, Connection c) {
return manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
}
}
@@ -0,0 +1,45 @@
package io.ebeaninternal.server.transaction;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.util.JdbcClose;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Aware of read only autoCommit based DataSource.
* <p>
* This means for implicit query only transactions we can:
* - Use the read only DataSource
* - Skip explicit commit (as we use AutoCommit instead)
* </p>
*/
class TransactionFactoryBasicWithRead extends TransactionFactoryBasic {
private final DataSource readOnlyDataSource;
TransactionFactoryBasicWithRead(TransactionManager manager, DataSourceSupplier dataSourceSupplier) {
super(manager, dataSourceSupplier);
this.readOnlyDataSource = dataSourceSupplier.getReadOnlyDataSource();
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
Connection connection = null;
try {
connection = readOnlyDataSource.getConnection();
return new ImplicitReadOnlyTransaction(connection);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
}
@@ -0,0 +1,30 @@
package io.ebeaninternal.server.transaction;
import io.ebean.config.CurrentTenantProvider;
/**
* Helper to build and return the appropriate TransactionFactory.
*/
class TransactionFactoryBuilder {
/**
* Build and return based on multi-tenancy and read only DataSource.
*/
static TransactionFactory build(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
boolean hasReadOnlyDataSource = dataSourceSupplier.getReadOnlyDataSource() != null;
if (tenantProvider == null) {
if (hasReadOnlyDataSource) {
return new TransactionFactoryBasicWithRead(manager, dataSourceSupplier);
} else {
return new TransactionFactoryBasic(manager, dataSourceSupplier);
}
} else {
if (hasReadOnlyDataSource) {
return new TransactionFactoryTenantWithRead(manager, dataSourceSupplier, tenantProvider);
} else {
return new TransactionFactoryTenant(manager, dataSourceSupplier, tenantProvider);
}
}
}
}
@@ -13,9 +13,9 @@ import java.sql.SQLException;
*/
class TransactionFactoryTenant extends TransactionFactory {
private final DataSourceSupplier dataSourceSupplier;
final DataSourceSupplier dataSourceSupplier;
private final CurrentTenantProvider tenantProvider;
final CurrentTenantProvider tenantProvider;
TransactionFactoryTenant(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
super(manager);
@@ -36,19 +36,19 @@ class TransactionFactoryTenant extends TransactionFactory {
}
private SpiTransaction create(int profileId, boolean explicit, Object tenantId) {
Connection c = null;
Connection connection = null;
try {
if (tenantId == null) {
// tenantId not set (by lazy loading) so get current tenantId
tenantId = tenantProvider.currentId();
}
c = dataSourceSupplier.getConnection(tenantId);
SpiTransaction transaction = manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
connection = dataSourceSupplier.getConnection(tenantId);
SpiTransaction transaction = manager.createTransaction(profileId, explicit, connection, counter.incrementAndGet());
transaction.setTenantId(tenantId);
return transaction;
} catch (PersistenceException ex) {
JdbcClose.close(c);
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
@@ -0,0 +1,45 @@
package io.ebeaninternal.server.transaction;
import io.ebean.config.CurrentTenantProvider;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.util.JdbcClose;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Aware of read only autoCommit based DataSource.
* <p>
* This means for implicit query only transactions we can:
* - Use the read only DataSource
* - Skip explicit commit (as we use AutoCommit instead)
* </p>
*/
class TransactionFactoryTenantWithRead extends TransactionFactoryTenant {
TransactionFactoryTenantWithRead(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
super(manager, dataSourceSupplier, tenantProvider);
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
Connection connection = null;
try {
if (tenantId == null) {
// tenantId not set (by lazy loading) so get current tenantId
tenantId = tenantProvider.currentId();
}
connection = dataSourceSupplier.getReadOnlyConnection(tenantId);
return new ImplicitReadOnlyTransaction(connection, tenantId);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
}
@@ -1,8 +1,8 @@
package io.ebeaninternal.server.transaction;
import io.ebean.BackgroundExecutor;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.annotation.PersistBatch;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import io.ebean.event.changelog.ChangeLogListener;
@@ -141,11 +141,7 @@ public class TransactionManager {
this.onQueryOnly = initOnQueryOnly(options.config.getDatabasePlatform().getOnQueryOnly());
CurrentTenantProvider tenantProvider = options.config.getCurrentTenantProvider();
if (tenantProvider == null) {
transactionFactory = new TransactionFactoryBasic(this, dataSourceSupplier);
} else {
transactionFactory = new TransactionFactoryTenant(this, dataSourceSupplier, tenantProvider);
}
this.transactionFactory = TransactionFactoryBuilder.build(this, dataSourceSupplier, tenantProvider);
}
/**
@@ -221,6 +217,10 @@ public class TransactionManager {
return dataSourceSupplier.getDataSource();
}
public DataSource getReadOnlyDataSource() {
return dataSourceSupplier.getReadOnlyDataSource();
}
/**
* Defines the type of behavior to use when closing a transaction that was used to query data only.
*/