#382 - ENH: Add support for @History using Oracle Total Recall ... for 'as of' queries

This commit is contained in:
Robin Bygrave
2015-08-18 22:19:55 +12:00
parent 18aeedc153
commit 10937de24b
13 changed files with 234 additions and 52 deletions
@@ -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.
* <p>
* 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.
* </p>
*/
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);
@@ -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;
}
}
@@ -0,0 +1,71 @@
package com.avaje.ebean.config.dbplatform;
/**
* Database view based implementation of DbHistorySupport.
* <p>
* These implementations have explicit history tables, a view to union the
* base table and history table and triggers to maintain the history.
* </p>
*/
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";
}
}
@@ -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";
}
}
@@ -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
@@ -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";
}
}
@@ -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 + ")";
}
}
@@ -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());
}
/**
@@ -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);
}
@@ -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.
*/
@@ -403,9 +403,10 @@ public class CQueryBuilder {
}
List<String> 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 {
@@ -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);
}
}
@@ -131,6 +131,18 @@ public class CQueryPredicates {
StringBuilder bindLog = new StringBuilder();
List<String> 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<String> 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);