diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java index a8127ac4f..fbd17114e 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java @@ -5,14 +5,32 @@ package com.avaje.ebean.config.dbplatform; */ public interface DbHistorySupport { + /** + * Return true if the 'As of' predicate is part of the from clause + * (more standard sql2011). So true for Oracle total recall and false + * for Postgres and MySql (where we use views and history tables). + */ + boolean isBindWithFromClause(); + /** * Return the number of columns bound in a 'As Of' predicate. *

- * Typically this is 2 but 1 for postgres using it's range type. + * This is 1 for more standard sql2011 style and Postgres which has the + * special range type and 2 for view based solutions with 2 columns such as + * MySql. *

*/ int getBindCount(); + /** + * For sql2011 style this ignores the passed in view suffix and returns something + * like the ' as of timestamp ?' clause to be appended after the base table name. + * + * @param asOfViewSuffix the configured view suffix (typically "_with_history"). + * @return The suffix appended after the base table name in the from and join clauses. + */ + String getAsOfViewSuffix(String asOfViewSuffix); + /** * Return the 'as of' predicate added for the given table alias. * @@ -26,7 +44,7 @@ public interface DbHistorySupport { * Return the column for the system period lower bound that will be included in findVersions() queries. * * @param tableAlias the table alias which will typically be 't0' - * @param sysPeriod the name of the sys_period column + * @param sysPeriod the name of the sys_period column */ String getSysPeriodLower(String tableAlias, String sysPeriod); @@ -34,7 +52,7 @@ public interface DbHistorySupport { * Return the column for the system period upper bound that will be included in findVersions() queries. * * @param tableAlias the table alias which will typically be 't0' - * @param sysPeriod the name of the sys_period column + * @param sysPeriod the name of the sys_period column */ String getSysPeriodUpper(String tableAlias, String sysPeriod); diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbStandardHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbStandardHistorySupport.java new file mode 100644 index 000000000..6d98b48c9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbStandardHistorySupport.java @@ -0,0 +1,32 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * SQL2011 based history support using 'as of timestamp' type clause appended as part of the the from or join clause. + */ +public abstract class DbStandardHistorySupport implements DbHistorySupport { + + /** + * Return true as with sql2011 the 'as of timestamp' clause included in from or join clause. + */ + @Override + public boolean isBindWithFromClause() { + return true; + } + + /** + * Return 1 as the bind count (not 2 for effective start and effective end columns). + */ + @Override + public int getBindCount() { + return 1; + } + + /** + * Return null - not used for sql2011 based history. + */ + @Override + public String getAsOfPredicate(String tableAlias, String sysPeriod) { + // not used for sql2011 based history + return null; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java new file mode 100644 index 000000000..3fe900179 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbViewHistorySupport.java @@ -0,0 +1,71 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Database view based implementation of DbHistorySupport. + *

+ * These implementations have explicit history tables, a view to union the + * base table and history table and triggers to maintain the history. + *

+ */ +public abstract class DbViewHistorySupport implements DbHistorySupport { + + /** + * Return false for view based implementations where we append extra 'as of' predicates to the end. + */ + @Override + public boolean isBindWithFromClause() { + return false; + } + + /** + * Returns the configured view suffix. + * + * @param asOfViewSuffix the configured view suffix (typically "_with_history"). + */ + @Override + public String getAsOfViewSuffix(String asOfViewSuffix) { + // just return the configured suffix + return asOfViewSuffix; + } + + /** + * Return 2 if we have effective start and effective end as 2 columns. + * Note that for postgres we can use a single range type so that returns 1. + */ + @Override + public int getBindCount() { + return 2; + } + + /** + * Return the 'as of' predicate clause appended to the end of the normal query predicates. + */ + @Override + public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) { + + // (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?)); + return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " < ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))"; + } + + /** + * Return the lower bound column prepended with the table alias. + * + * @param tableAlias the table alias + * @param sysPeriod the name of the sys_period column + */ + @Override + public String getSysPeriodLower(String tableAlias, String sysPeriod) { + return tableAlias + "." + sysPeriod + "_start"; + } + + /** + * Return the upper bound column prepended with the table alias. + * + * @param tableAlias the table alias + * @param sysPeriod the name of the sys_period column + */ + @Override + public String getSysPeriodUpper(String tableAlias, String sysPeriod) { + return tableAlias + "." + sysPeriod + "_end"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlHistorySupport.java index cc36972ef..616b7bfd1 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlHistorySupport.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlHistorySupport.java @@ -3,33 +3,7 @@ package com.avaje.ebean.config.dbplatform; /** * Runtime support for @History with MySql. */ -public class MySqlHistorySupport implements DbHistorySupport { +public class MySqlHistorySupport extends DbViewHistorySupport { - @Override - public int getBindCount() { - return 2; - } - @Override - public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) { - - StringBuilder sb = new StringBuilder(90); - sb.append("("); - sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append("_start").append(" < ? and ("); - sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append("_end").append(" is null or "); - sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append("_end").append(" > ?))"); - - // (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?)); - return sb.toString(); - } - - @Override - public String getSysPeriodLower(String tableAlias, String sysPeriod) { - return tableAlias+"."+sysPeriod+"_start"; - } - - @Override - public String getSysPeriodUpper(String tableAlias, String sysPeriod) { - return tableAlias+"."+sysPeriod+"_end"; - } } diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java index 6e639405c..43a2c6c1a 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java @@ -21,6 +21,7 @@ public class Oracle10Platform extends DatabasePlatform { this.dbEncrypt = new Oracle10DbEncrypt(); this.sqlLimiter = new RownumSqlLimiter(); this.platformDdl = new Oracle10Ddl(this.dbTypeMap, this.dbIdentity); + this.historySupport = new OracleDbHistorySupport(); // Not using getGeneratedKeys as instead we will // batch load sequences which enables JDBC batch execution diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/OracleDbHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/OracleDbHistorySupport.java new file mode 100644 index 000000000..0ebd68738 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/OracleDbHistorySupport.java @@ -0,0 +1,32 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Oracle Total recall based history support. + */ +public class OracleDbHistorySupport extends DbStandardHistorySupport { + + /** + * Return the ' as of timestamp ?' clause appended after the table name. + */ + @Override + public String getAsOfViewSuffix(String asOfViewSuffix) { + return " as of TIMESTAMP ?"; + } + + /** + * Returns the Oracle specific effective start column. + */ + @Override + public String getSysPeriodLower(String tableAlias, String sysPeriod) { + return "versions_starttime"; + } + + /** + * Returns the Oracle specific effective end column. + */ + @Override + public String getSysPeriodUpper(String tableAlias, String sysPeriod) { + return "versions_endtime"; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java index 5357a7fe8..347cc7954 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java @@ -3,10 +3,10 @@ package com.avaje.ebean.config.dbplatform; /** * Postgres support for history features. */ -public class PostgresHistorySupport implements DbHistorySupport { +public class PostgresHistorySupport extends DbViewHistorySupport { /** - * Return 1 as we are using the range type and hence don't need 2 bind variables. + * Return 1 as we are using the postgres range type and hence don't need 2 bind variables. */ @Override public int getBindCount() { @@ -24,19 +24,16 @@ public class PostgresHistorySupport implements DbHistorySupport { // for Postgres we are using the 'timestamp with timezone range' data type // as our sys_period column so hence the predicate below - //noinspection StringBufferReplaceableByString - StringBuilder sb = new StringBuilder(40); - sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append(" @> ?::timestamptz"); - return sb.toString(); + return asOfTableAlias + "." + asOfSysPeriod + " @> ?::timestamptz"; } @Override public String getSysPeriodLower(String tableAlias, String sysPeriod) { - return "lower("+tableAlias+"."+sysPeriod+")"; + return "lower(" + tableAlias + "." + sysPeriod + ")"; } @Override public String getSysPeriodUpper(String tableAlias, String sysPeriod) { - return "upper("+tableAlias+"."+sysPeriod+")"; + return "upper(" + tableAlias + "." + sysPeriod + ")"; } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index ec63c87a0..227f16435 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -122,7 +122,7 @@ public class InternalConfiguration { DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform(); - this.binder = new Binder(typeManager, getAsOfBindCount(databasePlatform)); + this.binder = getBinder(typeManager, databasePlatform); this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod()); ExternalTransactionManager externalTransactionManager = serverConfig.getExternalTransactionManager(); @@ -142,9 +142,12 @@ public class InternalConfiguration { /** * For 'As Of' queries return the number of bind variables per predicate. */ - private int getAsOfBindCount(DatabasePlatform databasePlatform) { + private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform) { DbHistorySupport historySupport = databasePlatform.getHistorySupport(); - return historySupport == null ? 0 : historySupport.getBindCount(); + if (historySupport == null) { + return new Binder(typeManager, 0, false); + } + return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause()); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java index 904b8425d..07894e988 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -12,6 +12,7 @@ import com.avaje.ebean.config.EncryptKeyManager; import com.avaje.ebean.config.NamingConvention; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.config.dbplatform.DbHistorySupport; import com.avaje.ebean.config.dbplatform.DbIdentity; import com.avaje.ebean.config.dbplatform.IdGenerator; import com.avaje.ebean.config.dbplatform.IdType; @@ -174,7 +175,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm()); this.eagerFetchLobs = serverConfig.isEagerFetchLobs(); - this.asOfViewSuffix = serverConfig.getAsOfViewSuffix(); + this.asOfViewSuffix = getAsOfViewSuffix(databasePlatform, serverConfig); this.readAnnotations = new ReadAnnotations(config.getGeneratedPropertyFactory(), asOfViewSuffix); this.bootupClasses = config.getBootupClasses(); this.createProperties = config.getDeployCreateProperties(); @@ -199,6 +200,17 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.transientProperties = new TransientProperties(); } + /** + * Return the AsOfViewSuffix allowing the DbHistorySupport to override the value in + * the case where there is no view (Oracle, DB2, MS SQL Server). + */ + private String getAsOfViewSuffix(DatabasePlatform databasePlatform, ServerConfig serverConfig) { + + DbHistorySupport historySupport = databasePlatform.getHistorySupport(); + // with historySupport returns a simple view suffix or the sql2011 as of timestamp suffix + return (historySupport == null ) ? serverConfig.getAsOfViewSuffix() : historySupport.getAsOfViewSuffix(serverConfig.getAsOfViewSuffix()); + } + public BeanDescriptor getBeanDescriptorById(String descriptorId) { return idDescMap.get(descriptorId); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java index ea28fd606..e2aeab3a5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java @@ -28,12 +28,15 @@ public class Binder { private final int asOfBindCount; + private final boolean bindAsOfWithFromClause; + /** * Set the PreparedStatement with which to bind variables to. */ - public Binder(TypeManager typeManager, int asOfBindCount) { + public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause) { this.typeManager = typeManager; this.asOfBindCount = asOfBindCount; + this.bindAsOfWithFromClause = bindAsOfWithFromClause; } /** @@ -43,6 +46,15 @@ public class Binder { return asOfBindCount; } + /** + * Return true if the 'as of' predicates are in the from/join clause in which case the timestamp is + * bound early (before all the other predicates ala Oracle). Return false if the 'as of' predicates are + * appended to the end of the predicates and the timestamp is bound last (Postgres, MySql). + */ + public boolean isBindAsOfWithFromClause() { + return bindAsOfWithFromClause; + } + /** * Bind the values to the Prepared Statement. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 8b0298a5f..5cb8ed158 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -403,9 +403,10 @@ public class CQueryBuilder { } List asOfTableAlias = query.getAsOfTableAlias(); - if (asOfTableAlias != null) { + if (asOfTableAlias != null && !historySupport.isBindAtFromClause()) { // append the effective date predicates for each table alias // that maps to a @History entity involved in this query + // Do this when history using separate tables/views (PG, MySql etc) if (!hasWhere) { sb.append(" where "); } else { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java index 89096dcb4..4f30e4929 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java @@ -30,22 +30,42 @@ public class CQueryHistorySupport { this.sysPeriod = sysPeriod; } + /** + * Return true if the bind of 'as of' timestamp occurs with the from clause + * rather than at the end. + */ + public boolean isBindAtFromClause() { + return dbHistorySupport.isBindWithFromClause(); + } + + /** + * Return the 'as of' history view for the given base table. + */ public String getAsOfView(String table) { return asOfTableMap.get(table); } + /** + * Return the lower bound column. + */ public String getSysPeriodLower(String tableAlias) { - return dbHistorySupport.getSysPeriodLower(tableAlias, sysPeriod); } + /** + * Return the upper bound column. + */ public String getSysPeriodUpper(String tableAlias) { - return dbHistorySupport.getSysPeriodUpper(tableAlias, sysPeriod); } + /** + * Return the predicate appended to the end of the query. + * + * Note used for Oracle total recall etc with the more standard approach. + */ public String getAsOfPredicate(String tableAlias) { - return dbHistorySupport.getAsOfPredicate(tableAlias, sysPeriod); } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java index 55532cd7b..d60a02403 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java @@ -131,6 +131,18 @@ public class CQueryPredicates { StringBuilder bindLog = new StringBuilder(); + List historyTableAlias = query.getAsOfTableAlias(); + if (historyTableAlias != null && binder.isBindAsOfWithFromClause()) { + // bind the asOf value for each table alias as part of the from/join clauses + // there is one effective date predicate per table alias + Timestamp asOf = query.getAsOf(); + bindLog.append("asOf ").append(asOf); + for (int i = 0; i < historyTableAlias.size() * binder.getAsOfBindCount(); i++) { + binder.bindObject(dataBind, asOf); + } + bindLog.append(", "); + } + if (idValue != null) { // this is a find by id type query... request.getBeanDescriptor().bindId(dataBind, idValue); @@ -143,7 +155,6 @@ public class CQueryPredicates { } if (whereExprBindValues != null) { - for (int i = 0; i < whereExprBindValues.size(); i++) { Object bindValue = whereExprBindValues.get(i); bindValue = binder.bindObject(dataBind, bindValue); @@ -155,7 +166,6 @@ public class CQueryPredicates { } if (filterManyExprBindValues != null) { - for (int i = 0; i < filterManyExprBindValues.size(); i++) { Object bindValue = filterManyExprBindValues.get(i); bindValue = binder.bindObject(dataBind, bindValue); @@ -166,9 +176,8 @@ public class CQueryPredicates { } } - List historyTableAlias = query.getAsOfTableAlias(); - if (historyTableAlias != null) { - // bind the asAt value for each table alias + if (historyTableAlias != null && !binder.isBindAsOfWithFromClause()) { + // bind the asOf value for each table alias after all the normal predicates // there is one effective date predicate per table alias Timestamp asOf = query.getAsOf(); bindLog.append(" asOf ").append(asOf);