From 932986a6b5b4a290fa7d6b0c084abc7d3fa933cc Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 9 Jan 2018 16:35:22 +1300 Subject: [PATCH] #1232 - Performance: Add support for a second read-only DataSource (for implicit query-only transactions) --- pom.xml | 8 +- .../java/io/ebean/config/ServerConfig.java | 77 +++ src/main/java/io/ebean/plugin/SpiServer.java | 5 + .../java/io/ebeaninternal/api/SpiQuery.java | 17 +- .../server/core/DefaultContainer.java | 20 +- .../server/core/DefaultServer.java | 8 +- .../server/core/InternalConfiguration.java | 6 +- .../core/MultiTenantDbCatalogSupplier.java | 54 +- .../core/MultiTenantDbSchemaSupplier.java | 53 +- .../server/core/MultiTenantDbSupplier.java | 11 + .../server/core/OrmQueryRequest.java | 10 +- .../server/core/SimpleDataSourceProvider.java | 18 +- .../expression/DefaultExpressionRequest.java | 16 +- .../ebeaninternal/server/persist/Binder.java | 16 + .../AutoCommitJdbcTransaction.java | 7 +- .../transaction/DataSourceSupplier.java | 17 + .../ImplicitReadOnlyTransaction.java | 591 ++++++++++++++++++ .../transaction/TransactionFactoryBasic.java | 40 +- .../TransactionFactoryBasicWithRead.java | 45 ++ .../TransactionFactoryBuilder.java | 30 + .../transaction/TransactionFactoryTenant.java | 12 +- .../TransactionFactoryTenantWithRead.java | 45 ++ .../transaction/TransactionManager.java | 12 +- .../io/ebean/config/ServerConfigTest.java | 7 + .../tests/basic/TestQueryWhereBetween.java | 73 ++- .../org/tests/query/TestQueryFindIterate.java | 24 +- src/test/resources/ebean.properties | 6 +- 27 files changed, 1128 insertions(+), 100 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java create mode 100644 src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasicWithRead.java create mode 100644 src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBuilder.java create mode 100644 src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenantWithRead.java diff --git a/pom.xml b/pom.xml index 35a1312b5..38967abb6 100644 --- a/pom.xml +++ b/pom.xml @@ -106,16 +106,10 @@ 3.5 - - org.avaje - avaje-datasource-api - 1.1 - - org.avaje avaje-datasource - 2.1.2 + 3.1.1 diff --git a/src/main/java/io/ebean/config/ServerConfig.java b/src/main/java/io/ebean/config/ServerConfig.java index e5784b14c..e2b1af685 100644 --- a/src/main/java/io/ebean/config/ServerConfig.java +++ b/src/main/java/io/ebean/config/ServerConfig.java @@ -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. + *

+ * Note that the DataSource is expected to use AutoCommit true mode avoiding the need + * for explicit commit (or rollback). + *

+ *

+ * 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. + *

+ */ + 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. + *

+ * This is only used if autoReadOnlyDataSource is true. + *

+ *

+ * 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. + *

+ */ + 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); diff --git a/src/main/java/io/ebean/plugin/SpiServer.java b/src/main/java/io/ebean/plugin/SpiServer.java index 956275646..4eb11837e 100644 --- a/src/main/java/io/ebean/plugin/SpiServer.java +++ b/src/main/java/io/ebean/plugin/SpiServer.java @@ -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(); + } diff --git a/src/main/java/io/ebeaninternal/api/SpiQuery.java b/src/main/java/io/ebeaninternal/api/SpiQuery.java index 9d96b7ef1..d257a4056 100644 --- a/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -98,17 +98,30 @@ public interface SpiQuery extends Query, 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() { diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java index 3afb745ea..1ef44d978 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java @@ -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); } /** diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 99fd5c3c4..d838c233e 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -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() { diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 92ca70639..881292bfe 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -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()); } } diff --git a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbCatalogSupplier.java b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbCatalogSupplier.java index 3caf7dbc3..11ede3e96 100644 --- a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbCatalogSupplier.java +++ b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbCatalogSupplier.java @@ -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()); } /** diff --git a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSchemaSupplier.java b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSchemaSupplier.java index d24652403..33f4de3ff 100644 --- a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSchemaSupplier.java +++ b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSchemaSupplier.java @@ -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. */ diff --git a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSupplier.java b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSupplier.java index 53472881d..b985c7864 100644 --- a/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSupplier.java +++ b/src/main/java/io/ebeaninternal/server/core/MultiTenantDbSupplier.java @@ -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); diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index a488c8d9d..d20453e8b 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -244,8 +244,14 @@ public final class OrmQueryRequest 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; } } diff --git a/src/main/java/io/ebeaninternal/server/core/SimpleDataSourceProvider.java b/src/main/java/io/ebeaninternal/server/core/SimpleDataSourceProvider.java index 7fda482c4..532f2f720 100644 --- a/src/main/java/io/ebeaninternal/server/core/SimpleDataSourceProvider.java +++ b/src/main/java/io/ebeaninternal/server/core/SimpleDataSourceProvider.java @@ -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); } diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java index c6eeca3c1..9f8cbb4ca 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -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() { diff --git a/src/main/java/io/ebeaninternal/server/persist/Binder.java b/src/main/java/io/ebeaninternal/server/persist/Binder.java index 9c673a5f3..7a69ee7eb 100644 --- a/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -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; } /** diff --git a/src/main/java/io/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java index 398e8a336..39acf81cc 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java +++ b/src/main/java/io/ebeaninternal/server/transaction/AutoCommitJdbcTransaction.java @@ -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 } diff --git a/src/main/java/io/ebeaninternal/server/transaction/DataSourceSupplier.java b/src/main/java/io/ebeaninternal/server/transaction/DataSourceSupplier.java index 406d34c6b..b95f035e1 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/DataSourceSupplier.java +++ b/src/main/java/io/ebeaninternal/server/transaction/DataSourceSupplier.java @@ -20,6 +20,15 @@ public interface DataSourceSupplier { */ DataSource getDataSource(); + /** + * Return the read only DataSource to use for the current request. + *

+ * 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. + *

+ */ + 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. */ diff --git a/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java new file mode 100644 index 000000000..e17444252 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -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. + *

+ * 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). + *

+ */ +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 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. + *

+ * Returning 0 implies to use the system wide default batch size. + *

+ */ + @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. + *

+ * This is general will result in a number of batched PreparedStatements + * executing. + *

+ */ + @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. + *

+ * 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; + } + + @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. + *

+ * This leaves the transaction active and expects another commit + * to occur later (which closes the underlying connection etc). + *

+ */ + @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(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasic.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasic.java index a10f3097c..d93c07753 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasic.java +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasic.java @@ -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()); + } + } diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasicWithRead.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasicWithRead.java new file mode 100644 index 000000000..920fd05de --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBasicWithRead.java @@ -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. + *

+ * This means for implicit query only transactions we can: + * - Use the read only DataSource + * - Skip explicit commit (as we use AutoCommit instead) + *

+ */ +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); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBuilder.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBuilder.java new file mode 100644 index 000000000..590fcec63 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryBuilder.java @@ -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); + } + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenant.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenant.java index b82ddc5c8..68a6ed579 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenant.java +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenant.java @@ -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) { diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenantWithRead.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenantWithRead.java new file mode 100644 index 000000000..15b26ca34 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionFactoryTenantWithRead.java @@ -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. + *

+ * This means for implicit query only transactions we can: + * - Use the read only DataSource + * - Skip explicit commit (as we use AutoCommit instead) + *

+ */ +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); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index 907ad1f63..126820fef 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -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. */ diff --git a/src/test/java/io/ebean/config/ServerConfigTest.java b/src/test/java/io/ebean/config/ServerConfigTest.java index d9e2918bb..dbeb9cdb8 100644 --- a/src/test/java/io/ebean/config/ServerConfigTest.java +++ b/src/test/java/io/ebean/config/ServerConfigTest.java @@ -1,6 +1,7 @@ package io.ebean.config; import io.ebean.annotation.PersistBatch; +import org.avaje.datasource.DataSourceConfig; import org.junit.Test; import java.util.Properties; @@ -27,6 +28,9 @@ public class ServerConfigTest { ServerConfig serverConfig = new ServerConfig(); serverConfig.setPersistBatch(PersistBatch.NONE); serverConfig.setPersistBatchOnCascade(PersistBatch.NONE); + serverConfig.setAutoReadOnlyDataSource(false); + serverConfig.setReadOnlyDataSource(null); + serverConfig.setReadOnlyDataSourceConfig(new DataSourceConfig()); Properties props = new Properties(); props.setProperty("persistBatch", "INSERT"); @@ -38,10 +42,13 @@ public class ServerConfigTest { props.setProperty("backgroundExecutorSchedulePoolSize", "4"); props.setProperty("dbOffline", "true"); props.setProperty("jsonDateTime", "ISO8601"); + props.setProperty("autoReadOnlyDataSource", "true"); serverConfig.loadFromProperties(props); assertTrue(serverConfig.isDbOffline()); + assertTrue(serverConfig.isAutoReadOnlyDataSource()); + assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatch()); assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatchOnCascade()); assertEquals(ServerConfig.DbUuid.BINARY, serverConfig.getDbTypeConfig().getDbUuid()); diff --git a/src/test/java/org/tests/basic/TestQueryWhereBetween.java b/src/test/java/org/tests/basic/TestQueryWhereBetween.java index 042ce5756..aef27680e 100644 --- a/src/test/java/org/tests/basic/TestQueryWhereBetween.java +++ b/src/test/java/org/tests/basic/TestQueryWhereBetween.java @@ -3,15 +3,19 @@ package org.tests.basic; import io.ebean.BaseTestCase; import io.ebean.Ebean; import io.ebean.Query; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; import org.tests.model.basic.Order; import org.tests.model.basic.ResetBasicData; -import org.junit.Assert; -import org.junit.Test; import java.sql.Timestamp; public class TestQueryWhereBetween extends BaseTestCase { + long statCount; + long statTotal; + @Test public void testCountOrderBy() { @@ -27,4 +31,69 @@ public class TestQueryWhereBetween extends BaseTestCase { String sql = query.getGeneratedSql(); Assert.assertTrue(sql.contains("between t0.cretime and t0.updtime")); } + + @Ignore + @Test + public void doStuff() { + + someLoop(3, true); + + int loop = 30000; + someLoop(loop, true); + someLoop(loop, true); + someLoop(loop, true); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + someLoop(loop); + + long avg = statTotal / statCount; + System.out.println("avg "+avg); + } + + private void someLoop(int loop) { + someLoop(loop, false); + } + private void someLoop(int loop, boolean warm) { + + long start = System.currentTimeMillis(); + for (int i = 0; i < loop; i++) { + someQuery(); + } + + long exe = System.currentTimeMillis() - start; + System.out.println("exe: "+exe); + + if (!warm) { + statTotal += exe; + statCount++; + } + } + + private void someQuery() { + + Timestamp t = new Timestamp(System.currentTimeMillis()); + + Query query = Ebean.find(Order.class).setAutoTune(false) + .where() + .le("cretime", t) + .order().asc("orderDate") + .order().desc("id"); + + query.findList(); + + } } diff --git a/src/test/java/org/tests/query/TestQueryFindIterate.java b/src/test/java/org/tests/query/TestQueryFindIterate.java index 6600315e3..de2c9f16b 100644 --- a/src/test/java/org/tests/query/TestQueryFindIterate.java +++ b/src/test/java/org/tests/query/TestQueryFindIterate.java @@ -5,13 +5,14 @@ import io.ebean.Ebean; import io.ebean.EbeanServer; import io.ebean.Query; import io.ebean.QueryIterator; +import io.ebean.plugin.SpiServer; +import org.avaje.datasource.DataSourcePool; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import org.tests.model.basic.OrderShipment; import org.tests.model.basic.ResetBasicData; -import org.avaje.datasource.DataSourcePool; -import org.ebeantest.LoggedSqlCollector; -import org.junit.Test; import javax.persistence.PersistenceException; import java.util.List; @@ -19,7 +20,10 @@ import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class TestQueryFindIterate extends BaseTestCase { @@ -225,11 +229,17 @@ public class TestQueryFindIterate extends BaseTestCase { } }); } - + @Test - public void testCloseConnection() throws Exception { + public void testCloseConnection() { ResetBasicData.reset(); - DataSourcePool dsPool = (DataSourcePool) server().getPluginApi().getDataSource(); + + SpiServer pluginApi = server().getPluginApi(); + DataSourcePool dsPool = (DataSourcePool) pluginApi.getServerConfig().getReadOnlyDataSource(); + if (dsPool == null) { + dsPool = (DataSourcePool) server().getPluginApi().getDataSource(); + } + int startConns = dsPool.getStatus(false).getBusy(); QueryIterator queryIterator = server().find(Customer.class) .where() diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index 3383f1d06..30158dc5e 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -20,6 +20,8 @@ ebean.ddl.run=true ebean.packages=org.tests datasource.default=h2 +ebean.autoReadOnlyDataSource=true + #ebean.persistBatch=NONE #ebean.h2.idType=SEQUENCE @@ -60,7 +62,7 @@ datasource.h2.minConnections=1 datasource.h2.maxConnections=25 #datasource.h2.heartbeatsql=select 1 #datasource.h2.isolationlevel=read_committed -datasource.h2.capturestacktrace=true +#datasource.h2.capturestacktrace=true datasource.h2.maxStackTraceSize=20 datasource.h2.poolListener=org.tests.basic.MyTestDataSourcePoolListener #datasource.h2.customProperties=IGNORECASE=TRUE;MODE=Oracle; @@ -121,7 +123,7 @@ datasource.pg.username=unit datasource.pg.password=unit datasource.pg.databaseUrl=jdbc:postgresql://127.0.0.1:5432/unit datasource.pg.databaseDriver=org.postgresql.Driver -datasource.pg.capturestacktrace=true +#datasource.pg.capturestacktrace=true datasource.pg.maxStackTraceSize=50 #TODO we need an sqlserver for travis - maybe from azure cloud