From 82489636d595ae8ad7625db7a969dd44fc570fbf Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Fri, 24 Jul 2015 10:18:17 +1200 Subject: [PATCH] #347 - @History support with query.asOf(Timestamp) --- .../java/com/avaje/ebean/ExpressionList.java | 12 +++++ src/main/java/com/avaje/ebean/Query.java | 12 +++++ .../com/avaje/ebean/annotation/History.java | 19 +++++++ .../ebean/annotation/HistoryExclude.java | 25 +++++++++ .../com/avaje/ebean/config/ServerConfig.java | 46 ++++++++++++++++ .../config/dbplatform/DatabasePlatform.java | 4 ++ .../config/dbplatform/PostgresPlatform.java | 16 +++++- .../com/avaje/ebeaninternal/api/SpiQuery.java | 50 ++++++++++++++++++ .../server/core/InternalConfiguration.java | 6 ++- .../server/deploy/BeanDescriptor.java | 52 +++++++++++++++++-- .../server/deploy/BeanDescriptorManager.java | 49 ++++++++++++----- .../server/deploy/BeanProperty.java | 11 ++++ .../server/deploy/DbSqlContext.java | 44 ++++++++-------- .../deploy/meta/DeployBeanDescriptor.java | 29 +++++++++-- .../deploy/meta/DeployBeanProperty.java | 10 ++++ .../deploy/parse/AnnotationAssocManys.java | 7 ++- .../server/deploy/parse/AnnotationClass.java | 13 ++++- .../server/deploy/parse/AnnotationFields.java | 5 ++ .../server/deploy/parse/ReadAnnotations.java | 14 ++++- .../server/expression/JunctionExpression.java | 6 +++ .../server/loadcontext/DLoadBeanContext.java | 1 + .../server/loadcontext/DLoadContext.java | 13 +++-- .../server/loadcontext/DLoadManyContext.java | 2 + .../server/query/CQueryBuilder.java | 48 ++++++++++++++--- .../server/query/CQueryEngine.java | 5 +- .../server/query/CQueryPredicates.java | 13 +++++ .../server/query/DefaultDbSqlContext.java | 42 +++++++++------ .../ebeaninternal/server/query/SqlTree.java | 8 +++ .../server/query/SqlTreeBuilder.java | 7 +-- .../server/query/SqlTreeNode.java | 6 +++ .../server/query/SqlTreeNodeBean.java | 37 ++++++++++++- .../server/query/SqlTreeNodeExtraJoin.java | 6 +++ .../server/query/SqlTreeNodeManyRoot.java | 2 +- .../query/SqlTreeNodeManyWhereJoin.java | 6 +++ .../server/query/SqlTreeNodeRoot.java | 14 ++--- .../server/querydefn/DefaultOrmQuery.java | 47 ++++++++++++++++- .../util/DefaultExpressionList.java | 6 +++ 37 files changed, 601 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/annotation/History.java create mode 100644 src/main/java/com/avaje/ebean/annotation/HistoryExclude.java diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 68bd217ad..4117f50cb 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -4,6 +4,7 @@ import com.avaje.ebean.text.PathProperties; import org.jetbrains.annotations.Nullable; import java.io.Serializable; +import java.sql.Timestamp; import java.util.Collection; import java.util.List; import java.util.Map; @@ -95,6 +96,17 @@ public interface ExpressionList extends Serializable { */ Query apply(PathProperties pathProperties); + /** + * Perform an 'As of' query using history tables to return the object graph + * as of a time in the past. + *

+ * To perform this query the DB must have underlying history tables. + *

+ * + * @param asOf the date time in the past at which you want to view the data + */ + Query asOf(Timestamp asOf); + /** * Execute the query iterating over the results. * diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index c7b96bdd7..03f4fd22e 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -4,6 +4,7 @@ import com.avaje.ebean.text.PathProperties; import org.jetbrains.annotations.Nullable; import java.io.Serializable; +import java.sql.Timestamp; import java.util.List; import java.util.Map; import java.util.Set; @@ -285,6 +286,17 @@ public interface Query extends Serializable { */ Query setRawSql(RawSql rawSql); + /** + * Perform an 'As of' query using history tables to return the object graph + * as of a time in the past. + *

+ * To perform this query the DB must have underlying history tables. + *

+ * + * @param asOf the date time in the past at which you want to view the data + */ + Query asOf(Timestamp asOf); + /** * Cancel the query execution if supported by the underlying database and * driver. diff --git a/src/main/java/com/avaje/ebean/annotation/History.java b/src/main/java/com/avaje/ebean/annotation/History.java new file mode 100644 index 000000000..36888086d --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/History.java @@ -0,0 +1,19 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an entity bean as having history support. + *

+ * In Postgres for example this means there is an associated history table which is + * typically automatically populated via database triggers. + *

+ */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface History { + +} diff --git a/src/main/java/com/avaje/ebean/annotation/HistoryExclude.java b/src/main/java/com/avaje/ebean/annotation/HistoryExclude.java new file mode 100644 index 000000000..a41dfe1d9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/HistoryExclude.java @@ -0,0 +1,25 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a property as being excluded from history. + *

+ * This means the property values are not maintained in the history table. Typically this + * would be placed on relatively large properties (Clobs, Blobs, large varchar columns etc) + * that are considered not interesting enough to maintain history on excluding them reduces + * underlying database costs. + *

+ *

+ * When placed on a ManyToMany this means that the intersection table does not have history + * support. + *

+ */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface HistoryExclude { + +} diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index 7dfb23ace..4f234a425 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -144,6 +144,18 @@ public class ServerConfig { */ private int databaseSequenceBatchSize = 20; + /** + * Suffix appended to the base table to derive the view that contains the union + * of the base table and the history table in order to support asOf queries. + */ + private String asOfViewSuffix = "_with_history"; + + /** + * Column used to support history and 'As of' queries. This column is a timestamp range + * or equivalent. + */ + private String asOfSysPeriod = "sys_period"; + /** * Use for transaction scoped batch mode. */ @@ -579,6 +591,38 @@ public class ServerConfig { this.databaseSequenceBatchSize = databaseSequenceBatchSize; } + /** + * Return the suffix appended to the base table to derive the view that contains the union + * of the base table and the history table in order to support asOf queries. + */ + public String getAsOfViewSuffix() { + return asOfViewSuffix; + } + + /** + * Set the suffix appended to the base table to derive the view that contains the union + * of the base table and the history table in order to support asOf queries. + */ + public void setAsOfViewSuffix(String asOfViewSuffix) { + this.asOfViewSuffix = asOfViewSuffix; + } + + /** + * Return the database column used to support history and 'As of' queries. This column is a timestamp range + * or equivalent. + */ + public String getAsOfSysPeriod() { + return asOfSysPeriod; + } + + /** + * Set the database column used to support history and 'As of' queries. This column is a timestamp range + * or equivalent. + */ + public void setAsOfSysPeriod(String asOfSysPeriod) { + this.asOfSysPeriod = asOfSysPeriod; + } + /** * Return true if we are running in a JTA Transaction manager. */ @@ -1809,6 +1853,8 @@ public class ServerConfig { persistenceContextScope = PersistenceContextScope.valueOf(p.get("persistenceContextScope", "TRANSACTION")); + asOfViewSuffix = p.get("asOfViewSuffix", asOfViewSuffix); + asOfSysPeriod = p.get("asOfSysPeriod", asOfSysPeriod); dataSourceJndiName = p.get("dataSourceJndiName", dataSourceJndiName); databaseSequenceBatchSize = p.getInt("databaseSequenceBatchSize", databaseSequenceBatchSize); databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue); diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java index c5b4413fd..149383c42 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -442,6 +442,10 @@ public class DatabasePlatform { return disallowBatchOnCascade; } + public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) { + throw new RuntimeException("AsOf query not support of this database platform yet"); + } + /** * Generate and return the create sequence DDL. */ diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java index d03d4e690..9e20cface 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -60,12 +60,24 @@ public class PostgresPlatform extends DatabasePlatform { } + /** + * Build and return the 'as of' predicate for a given table alias. + *

+ * Each @History entity involved in the query has this predicate added using the related table alias. + *

+ */ + public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) { + + StringBuilder sb = new StringBuilder(40); + sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append(" @> ?::timestamptz"); + return sb.toString(); + } + /** * Create a Postgres specific sequence IdGenerator. */ @Override - public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, - String seqName, int batchSize) { + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { return new PostgresSequenceIdGenerator(be, ds, seqName, batchSize); } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 2621fc4bd..c604174be 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -19,6 +19,7 @@ import com.avaje.ebeaninternal.server.querydefn.NaturalKeyBindParam; import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail; import com.avaje.ebeaninternal.server.querydefn.OrmQueryProperties; +import java.sql.Timestamp; import java.util.List; /** @@ -86,6 +87,30 @@ public interface SpiQuery extends Query { SUBQUERY } + enum TemporalMode { + /** + * Query runs against current data (normal). + */ + CURRENT, + + /** + * Query runs potentially returning many versions of the same bean. + */ + VERSIONS, + + /** + * Query runs 'As Of' a given date time. + */ + AS_OF; + + /** + * Return the mode of the query of if null return CURRENT mode. + */ + public static TemporalMode of(SpiQuery query) { + return (query != null) ? query.getTemporalMode() : TemporalMode.CURRENT; + } + } + /** * Return the PersistenceContextScope that this query should use. *

@@ -115,6 +140,31 @@ public interface SpiQuery extends Query { */ Mode getMode(); + /** + * Return the Temporal mode for the query. + */ + TemporalMode getTemporalMode(); + + /** + * Return true if this is a 'As Of' query. + */ + boolean isAsOfQuery(); + + /** + * Return the asOf Timestamp which the query should run as. + */ + Timestamp getAsOf(); + + /** + * Add a table alias for a @History entity involved in a 'As Of' query. + */ + void addAsOfTableAlias(String tableAlias); + + /** + * Return the list of table alias involved in a 'As Of' query that have @History support. + */ + List getAsOfTableAlias(); + /** * Return a listener that wants to be notified when the bean collection is * first used. 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 df971d8db..74a56c0a5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -41,6 +41,8 @@ import com.avaje.ebeaninternal.server.type.DefaultTypeManager; import com.avaje.ebeaninternal.server.type.TypeManager; import com.fasterxml.jackson.core.JsonFactory; +import java.util.Map; + /** * Used to extend the ServerConfig with additional objects used to configure and * construct an EbeanServer. @@ -116,11 +118,11 @@ public class InternalConfiguration { this.deployUtil = new DeployUtil(typeManager, serverConfig); this.beanDescriptorManager = new BeanDescriptorManager(this); - beanDescriptorManager.deploy(); + Map asOfTableMapping = beanDescriptorManager.deploy(); this.transactionManager = createTransactionManager(); - this.cQueryEngine = new CQueryEngine(serverConfig.getDatabasePlatform(), binder); + this.cQueryEngine = new CQueryEngine(serverConfig.getDatabasePlatform(), binder, asOfTableMapping, serverConfig.getAsOfSysPeriod()); ExternalTransactionManager externalTransactionManager = serverConfig.getExternalTransactionManager(); if (externalTransactionManager == null && serverConfig.isUseJtaTransactionManager()) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 6d677f920..129497df3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -112,6 +112,8 @@ public class BeanDescriptor implements MetaBeanInfo { * The base database table. */ private final String baseTable; + private final String baseTableAsOf; + private final boolean historySupport; /** * Map of BeanProperty Linked so as to preserve order. @@ -332,8 +334,9 @@ public class BeanDescriptor implements MetaBeanInfo { this.updateChangesOnly = deploy.isUpdateChangesOnly(); this.compoundUniqueConstraints = deploy.getCompoundUniqueConstraints(); + this.historySupport = deploy.isHistorySupport(); this.baseTable = InternString.intern(deploy.getBaseTable()); - + this.baseTableAsOf = deploy.getBaseTableAsOf(); this.autoFetchTunable = EntityType.ORM.equals(entityType) && (beanFinder == null); // helper object used to derive lists of properties @@ -496,13 +499,19 @@ public class BeanDescriptor implements MetaBeanInfo { * These properties need to be initialised prior to the association properties * as they are used to get the imported and exported properties. *

+ * @param withHistoryTables map populated if @History is supported on this entity bean */ - public void initialiseId() { + public void initialiseId(Map withHistoryTables) { if (logger.isTraceEnabled()) { logger.trace("BeanDescriptor initialise " + fullName); } + if (historySupport) { + // add mapping (used to swap out baseTable for asOf queries) + withHistoryTables.put(baseTable, baseTableAsOf); + } + if (inheritInfo != null) { inheritInfo.setDescriptor(this); } @@ -522,8 +531,25 @@ public class BeanDescriptor implements MetaBeanInfo { /** * Initialise the exported and imported parts for associated properties. + * + * @param asOfTableMap the map of base tables to associated 'with history' tables + * @param asOfViewSuffix the suffix added to the table name to derive the 'with history' view name */ - public void initialiseOther() { + public void initialiseOther(Map asOfTableMap, String asOfViewSuffix) { + + if (historySupport) { + // history support on this bean so check all associated intersection tables + // and if they are not excluded register the associated 'with history' table + for (int i = 0; i < propertiesManyToMany.length; i++) { + if (!propertiesManyToMany[i].isExcludedFromHistory()) { + // this intersection table has history support so also register + // it into the asOfTableMap + TableJoin intersectionTableJoin = propertiesManyToMany[i].getIntersectionTableJoin(); + String intersectionTableName = intersectionTableJoin.getTable(); + asOfTableMap.put(intersectionTableName, intersectionTableName + asOfViewSuffix); + } + } + } if (!isEmbedded()) { // initialise all the non-id properties @@ -761,7 +787,7 @@ public class BeanDescriptor implements MetaBeanInfo { } public void cacheBeanPut(T bean) { - cacheBeanPutData((EntityBean)bean); + cacheBeanPutData((EntityBean) bean); } /** @@ -1628,6 +1654,24 @@ public class BeanDescriptor implements MetaBeanInfo { return baseTable; } + /** + * Return the base table to use given the query temporal mode. + */ + public String getBaseTable(SpiQuery.TemporalMode mode) { + switch (mode) { + case VERSIONS: return baseTableAsOf; + case AS_OF: return baseTableAsOf; + default: return baseTable; + } + } + + /** + * Return true if this entity bean has history support. + */ + public boolean isHistorySupport() { + return historySupport; + } + /** * Return the identity generation type. */ 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 6ba7f7e6c..00bf46624 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -10,6 +10,7 @@ import com.avaje.ebean.cache.ServerCacheManager; import com.avaje.ebean.config.EncryptKey; 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.DbIdentity; import com.avaje.ebean.config.dbplatform.IdGenerator; @@ -51,7 +52,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { private static final BeanDescComparator beanDescComparator = new BeanDescComparator(); - private final ReadAnnotations readAnnotations = new ReadAnnotations(); + private final ReadAnnotations readAnnotations; private final TransientProperties transientProperties; @@ -127,34 +128,45 @@ public class BeanDescriptorManager implements BeanDescriptorMap { private final boolean eagerFetchLobs; + private final String asOfViewSuffix; + + /** + * Map of base tables to 'with history views' used to support 'as of' queries. + */ + private final Map asOfTableMap = new HashMap(); + /** * Create for a given database dbConfig. */ public BeanDescriptorManager(InternalConfiguration config) { - this.serverName = InternString.intern(config.getServerConfig().getName()); + ServerConfig serverConfig = config.getServerConfig(); + + this.serverName = InternString.intern(serverConfig.getName()); this.cacheManager = config.getCacheManager(); this.xmlConfig = config.getXmlConfig(); - this.dbSequenceBatchSize = config.getServerConfig().getDatabaseSequenceBatchSize(); + this.dbSequenceBatchSize = serverConfig.getDatabaseSequenceBatchSize(); this.backgroundExecutor = config.getBackgroundExecutor(); - this.dataSource = config.getServerConfig().getDataSource(); - this.encryptKeyManager = config.getServerConfig().getEncryptKeyManager(); - this.databasePlatform = config.getServerConfig().getDatabasePlatform(); + this.dataSource = serverConfig.getDataSource(); + this.encryptKeyManager = serverConfig.getEncryptKeyManager(); + this.databasePlatform = serverConfig.getDatabasePlatform(); this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm()); - this.eagerFetchLobs = config.getServerConfig().isEagerFetchLobs(); + this.eagerFetchLobs = serverConfig.isEagerFetchLobs(); + this.asOfViewSuffix = serverConfig.getAsOfViewSuffix(); + this.readAnnotations = new ReadAnnotations(asOfViewSuffix); this.bootupClasses = config.getBootupClasses(); this.createProperties = config.getDeployCreateProperties(); this.typeManager = config.getTypeManager(); - this.namingConvention = config.getServerConfig().getNamingConvention(); + this.namingConvention = serverConfig.getNamingConvention(); this.dbIdentity = config.getDatabasePlatform().getDbIdentity(); this.deplyInherit = config.getDeployInherit(); this.deployOrmXml = config.getDeployOrmXml(); this.deployUtil = config.getDeployUtil(); - this.beanManagerFactory = new BeanManagerFactory(config.getServerConfig(), config.getDatabasePlatform()); + this.beanManagerFactory = new BeanManagerFactory(serverConfig, config.getDatabasePlatform()); - this.updateChangesOnly = config.getServerConfig().isUpdateChangesOnly(); + this.updateChangesOnly = serverConfig.isUpdateChangesOnly(); this.beanLifecycleAdapterFactory = new BeanLifecycleAdapterFactory(); this.persistControllerManager = new PersistControllerManager(bootupClasses); @@ -205,7 +217,10 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return idBinderFactory.createIdBinder(idProperty); } - public void deploy() { + /** + * Deploy returning the asOfTableMap (which is required by the SQL builders). + */ + public Map deploy() { try { createListeners(); @@ -237,6 +252,9 @@ public class BeanDescriptorManager implements BeanDescriptorMap { deplyInfoMap.clear(); deplyInfoMap = null; + + return asOfTableMap; + } catch (RuntimeException e) { String msg = "Error in deployment"; logger.error(msg, e); @@ -324,7 +342,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { // first (as they are needed to initialise the // associated properties in the second pass). for (BeanDescriptor d : descMap.values()) { - d.initialiseId(); + d.initialiseId(asOfTableMap); } // PASS 2: @@ -336,7 +354,10 @@ public class BeanDescriptorManager implements BeanDescriptorMap { // PASS 3: // now initialise all the associated properties for (BeanDescriptor d : descMap.values()) { - d.initialiseOther(); + // also look for intersection tables with + // associated history support and register them + // into the asOfTableMap + d.initialiseOther(asOfTableMap, asOfViewSuffix); } // create BeanManager for each non-embedded entity bean @@ -973,7 +994,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { if (!EntityType.ORM.equals(desc.getEntityType())) { // not using base table - desc.setBaseTable(null); + desc.setBaseTable(null, null); } // mark transient properties diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index d3acb11a4..5163980d3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -173,6 +173,8 @@ public class BeanProperty implements ElPropertyValue { */ final int dbType; + final boolean excludedFromHistory; + /** * The default value to insert if null. */ @@ -256,6 +258,7 @@ public class BeanProperty implements ElPropertyValue { this.dbRead = deploy.isDbRead(); this.dbInsertable = deploy.isDbInsertable(); this.dbUpdatable = deploy.isDbUpdateable(); + this.excludedFromHistory = deploy.isExcludedFromHistory(); this.secondaryTable = deploy.isSecondaryTable(); if (secondaryTable) { @@ -341,6 +344,7 @@ public class BeanProperty implements ElPropertyValue { this.sqlFormulaSelect = InternString.intern(override.getSqlFormulaSelect()); this.formula = sqlFormulaSelect != null; + this.excludedFromHistory = source.excludedFromHistory; this.fetchEager = source.fetchEager; this.unidirectionalShadow = source.unidirectionalShadow; this.discriminator = source.discriminator; @@ -1004,6 +1008,13 @@ public class BeanProperty implements ElPropertyValue { return dbEncryptedType; } + /** + * Return true if this property is excluded from history. + */ + public boolean isExcludedFromHistory() { + return excludedFromHistory; + } + /** * Return true if this property should be included in an Insert. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java index 4ae93ea88..36b6d366c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java @@ -10,108 +10,108 @@ public interface DbSqlContext { /** * Add a join to the sql query. */ - public void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2, String inheritance); + void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2, String inheritance); - public void pushSecondaryTableAlias(String alias); + void pushSecondaryTableAlias(String alias); /** * Push the current table alias onto the stack. */ - public void pushTableAlias(String tableAlias); + void pushTableAlias(String tableAlias); /** * Pop the current table alias from the stack. */ - public void popTableAlias(); + void popTableAlias(); /** * Add an encrypted property which will require additional binding. */ - public void addEncryptedProp(BeanProperty prop); + void addEncryptedProp(BeanProperty prop); /** * Return a list of encrypted properties which require additional binding. */ - public BeanProperty[] getEncryptedProps(); + BeanProperty[] getEncryptedProps(); /** * Append a char directly to the SQL buffer. */ - public DbSqlContext append(char s); + DbSqlContext append(char s); /** * Append a string directly to the SQL buffer. */ - public DbSqlContext append(String s); + DbSqlContext append(String s); /** * Peek the current table alias. */ - public String peekTableAlias(); + String peekTableAlias(); /** * Add a raw column to the sql. */ - public void appendRawColumn(String rawcolumnWithTableAlias); + void appendRawColumn(String rawcolumnWithTableAlias); /** * Append a column with an explicit table alias. */ - public void appendColumn(String tableAlias, String column); + void appendColumn(String tableAlias, String column); /** * Append a column with the current table alias. */ - public void appendColumn(String column); + void appendColumn(String column); /** * Append a Sql Formula select. This converts the "${ta}" keyword to the * current table alias. */ - public void appendFormulaSelect(String sqlFormulaSelect); + void appendFormulaSelect(String sqlFormulaSelect); /** * Append a Sql Formula join. This converts the "${ta}" keyword to the current * table alias. */ - public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType); + void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType); /** * Return the current content length. */ - public int length(); + int length(); /** * Return the current context of the sql context. */ - public String getContent(); + String getContent(); /** * Return the current join node. */ - public String peekJoin(); + String peekJoin(); /** * Push a join node onto the stack. */ - public void pushJoin(String prefix); + void pushJoin(String prefix); /** * Pop a join node off the stack. */ - public void popJoin(); + void popJoin(); /** * Return a table alias without many where clause joins. Typically this is for * the select clause (fetch joins). */ - public String getTableAlias(String prefix); + String getTableAlias(String prefix); /** * Return a table alias that takes into account many where joins. */ - public String getTableAliasManyWhere(String prefix); + String getTableAliasManyWhere(String prefix); - public String getRelativePrefix(String propName); + String getRelativePrefix(String propName); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 9b7fa2ef0..e7622bf8b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -87,6 +87,11 @@ public class DeployBeanDescriptor { * The base database table. */ private String baseTable; + + private String baseTableAsOf; + + private boolean historySupport; + private TableName baseTableFull; private String[] properties; @@ -135,6 +140,20 @@ public class DeployBeanDescriptor { return Modifier.isAbstract(beanType.getModifiers()); } + /** + * Set to true for @History entity beans that have history. + */ + public void setHistorySupport(boolean historySupport) { + this.historySupport = historySupport; + } + + /** + * Return true if this is an @History entity bean. + */ + public boolean isHistorySupport() { + return historySupport; + } + public boolean isScalaObject() { Class[] interfaces = beanType.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { @@ -405,6 +424,10 @@ public class DeployBeanDescriptor { return baseTable; } + public String getBaseTableAsOf() { + return baseTableAsOf; + } + /** * Return the base table with full structure. */ @@ -413,12 +436,12 @@ public class DeployBeanDescriptor { } /** - * Set the base table. Only properties mapped to the base table are by default - * persisted. + * Set the base table. Only properties mapped to the base table are by default persisted. */ - public void setBaseTable(TableName baseTableFull) { + public void setBaseTable(TableName baseTableFull, String asOfSuffix) { this.baseTableFull = baseTableFull; this.baseTable = baseTableFull == null ? null : baseTableFull.getQualifiedName(); + this.baseTableAsOf = baseTable + asOfSuffix; } public void sortProperties() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index e43c41ff0..cc741aa31 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -215,6 +215,8 @@ public class DeployBeanProperty { private boolean indexed; private String indexName; + private boolean excludedFromHistory; + public DeployBeanProperty(DeployBeanDescriptor desc, Class propertyType, ScalarType scalarType, ScalarTypeConverter typeConverter) { this.desc = desc; this.propertyType = propertyType; @@ -912,4 +914,12 @@ public class DeployBeanProperty { public void setIndexName(String indexName) { this.indexName = indexName; } + + public boolean isExcludedFromHistory() { + return excludedFromHistory; + } + + public void setExcludedFromHistory(boolean excludedFromHistory) { + this.excludedFromHistory = excludedFromHistory; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java index 98f2d2b8b..665dff5b6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -8,6 +8,7 @@ import javax.persistence.MapKey; import javax.persistence.OneToMany; import javax.persistence.OrderBy; +import com.avaje.ebean.annotation.HistoryExclude; import com.avaje.ebean.annotation.PrivateOwned; import com.avaje.ebean.annotation.Where; import com.avaje.ebean.bean.BeanCollection.ModifyListenMode; @@ -65,7 +66,11 @@ public class AnnotationAssocManys extends AnnotationParser { readToMany(manyToMany, prop); } - OrderBy orderBy = get(prop, OrderBy.class); + if (get(prop, HistoryExclude.class) != null) { + prop.setExcludedFromHistory(true); + } + + OrderBy orderBy = get(prop, OrderBy.class); if (orderBy != null) { prop.setFetchOrderBy(orderBy.value()); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java index 9a114a67a..7f4dc21b1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -10,6 +10,7 @@ import javax.persistence.UniqueConstraint; import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.annotation.CacheTuning; import com.avaje.ebean.annotation.EntityConcurrencyMode; +import com.avaje.ebean.annotation.History; import com.avaje.ebean.annotation.NamedUpdate; import com.avaje.ebean.annotation.NamedUpdates; import com.avaje.ebean.annotation.UpdateMode; @@ -26,8 +27,11 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty; */ public class AnnotationClass extends AnnotationParser { - public AnnotationClass(DeployBeanInfo info) { + private final String asOfViewSuffix; + + public AnnotationClass(DeployBeanInfo info, String asOfViewSuffix) { super(info); + this.asOfViewSuffix = asOfViewSuffix; } /** @@ -48,7 +52,7 @@ public class AnnotationClass extends AnnotationParser { // default the TableName using NamingConvention. TableName tableName = namingConvention.getTableName(descriptor.getBeanType()); - descriptor.setBaseTable(tableName); + descriptor.setBaseTable(tableName, asOfViewSuffix); } } @@ -84,6 +88,11 @@ public class AnnotationClass extends AnnotationParser { } } + History history = cls.getAnnotation(History.class); + if (history != null) { + descriptor.setHistorySupport(true); + } + UpdateMode updateMode = cls.getAnnotation(UpdateMode.class); if (updateMode != null) { descriptor.setUpdateChangesOnly(updateMode.updateChangesOnly()); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 7fe8323d7..d262bc19a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -8,6 +8,7 @@ import com.avaje.ebean.annotation.EmbeddedColumns; import com.avaje.ebean.annotation.Encrypted; import com.avaje.ebean.annotation.Expose; import com.avaje.ebean.annotation.Formula; +import com.avaje.ebean.annotation.HistoryExclude; import com.avaje.ebean.annotation.Index; import com.avaje.ebean.annotation.JsonIgnore; import com.avaje.ebean.annotation.UpdatedTimestamp; @@ -217,6 +218,10 @@ public class AnnotationFields extends AnnotationParser { generatedPropFactory.setUpdateTimestamp(prop); } + if (get(prop, HistoryExclude.class) != null) { + prop.setExcludedFromHistory(true); + } + if (validationAnnotations) { NotNull notNull = get(prop, NotNull.class); if (notNull != null && isNotNullOnAllValidationGroups(notNull.groups())) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/ReadAnnotations.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/ReadAnnotations.java index eaafc12dd..2ca913b53 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/ReadAnnotations.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/ReadAnnotations.java @@ -8,7 +8,17 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; */ public class ReadAnnotations { - /** + /** + * Typically _with_history and when appended to the base table derives the name of + * the view that unions the base table with the history table to support asOf queries. + */ + private final String asOfViewSuffix; + + public ReadAnnotations(String asOfViewSuffix) { + this.asOfViewSuffix = asOfViewSuffix; + } + + /** * Read the initial non-relationship annotations included Id and EmbeddedId. *

* We then have enough to create BeanTables which are used in readAssociations @@ -18,7 +28,7 @@ public class ReadAnnotations { public void readInitial(DeployBeanInfo info, boolean eagerFetchLobs){ try { - new AnnotationClass(info).parse(); + new AnnotationClass(info, asOfViewSuffix).parse(); new AnnotationFields(info, eagerFetchLobs).parse(); } catch (RuntimeException e){ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java index 7400ad8ff..19a0fae3c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.expression; +import java.sql.Timestamp; import java.util.Collection; import java.util.List; import java.util.Map; @@ -217,6 +218,11 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr throw new RuntimeException("filterMany not allowed on Junction expression list"); } + @Override + public Query asOf(Timestamp asOf) { + return exprList.asOf(asOf); + } + @Override public Query apply(PathProperties pathProperties) { return exprList.apply(pathProperties); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java index dcb611f54..dbcdd61ec 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -49,6 +49,7 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex if (parent.isReadOnly() != null) { query.setReadOnly(parent.isReadOnly()); } + query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); query.setLazyLoadProperty(lazyLoadProperty); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java index a24ccca51..1b6d6ea14 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java @@ -13,6 +13,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.querydefn.OrmQueryProperties; +import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,7 +31,8 @@ public class DLoadContext implements LoadContext { private final Map manyMap = new HashMap(); private final DLoadBeanContext rootBeanContext; - + + private final Timestamp asOf; private final Boolean readOnly; private final boolean excludeBeanCache; private final int defaultBatchSize; @@ -56,6 +58,7 @@ public class DLoadContext implements LoadContext { this.rootDescriptor = request.getBeanDescriptor(); SpiQuery query = request.getQuery(); + this.asOf = query.getAsOf(); this.readOnly = query.isReadOnly(); this.excludeBeanCache = Boolean.FALSE.equals(query.isUseBeanCache()); this.useAutofetchManager = query.getAutoFetchManager() != null; @@ -211,8 +214,12 @@ public class DLoadContext implements LoadContext { protected Boolean isReadOnly() { return readOnly; } - - public PersistenceContext getPersistenceContext() { + + protected Timestamp getAsOf() { + return asOf; + } + + public PersistenceContext getPersistenceContext() { return persistenceContext; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java index 004727d5c..7f4739e33 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -60,6 +60,8 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex if (parent.isReadOnly() != null){ query.setReadOnly(parent.isReadOnly()); } + + query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); if (queryProps != null){ 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 79e14ef88..938bd3c10 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -23,6 +23,8 @@ import com.avaje.ebeaninternal.server.querydefn.OrmQueryLimitRequest; import javax.persistence.PersistenceException; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -43,14 +45,20 @@ public class CQueryBuilder implements Constants { private final boolean selectCountWithAlias; + private final Map asOfTableMapping; + + private final String asOfSysPeriod; + private DatabasePlatform dbPlatform; /** * Create the SqlGenSelect. */ - public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder) { + public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, Map asOfTableMapping, String asOfSysPeriod) { this.binder = binder; + this.asOfTableMapping = asOfTableMapping; + this.asOfSysPeriod = asOfSysPeriod; this.tableAliasPlaceHolder = dbPlatform.getTableAliasPlaceHolder(); this.columnAliasPrefix = dbPlatform.getColumnAliasPrefix(); this.sqlSelectBuilder = new RawSqlSelectClauseBuilder(dbPlatform, binder); @@ -103,7 +111,8 @@ public class CQueryBuilder implements Constants { // use RawSql or generated Sql predicates.prepare(true); - SqlTree sqlTree = createSqlTree(request, predicates); + Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; + SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); SqlLimitResponse s = buildSql(null, request, predicates, sqlTree); String sql = s.getSql(); @@ -155,7 +164,8 @@ public class CQueryBuilder implements Constants { predicates.prepare(true); - SqlTree sqlTree = createSqlTree(request, predicates); + Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; + SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); SqlLimitResponse s = buildSql(sqlSelect, request, predicates, sqlTree); String sql = s.getSql(); if (hasMany || query.isRawSql()) { @@ -203,7 +213,14 @@ public class CQueryBuilder implements Constants { predicates.prepare(true); // Build the tree structure that represents the query. - SqlTree sqlTree = createSqlTree(request, predicates); + SpiQuery query = request.getQuery(); + + Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; + SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); + if (query.isAsOfQuery()) { + sqlTree.addAsOfTableAlias(query); + } + SqlLimitResponse res = buildSql(null, request, predicates, sqlTree); boolean rawSql = request.isRawSql(); @@ -232,13 +249,13 @@ public class CQueryBuilder implements Constants { * order by clauses that are not already included for the select clause. *

*/ - private SqlTree createSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { + private SqlTree createSqlTree(OrmQueryRequest request, CQueryPredicates predicates, Map withHistoryTables) { if (request.isRawSql()) { return createRawSqlSqlTree(request, predicates); } - return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates).build(); + return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, withHistoryTables).build(); } private SqlTree createRawSqlSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { @@ -380,6 +397,7 @@ public class CQueryBuilder implements Constants { String dbFilterMany = predicates.getDbFilterMany(); if (!isEmpty(dbFilterMany)) { if (!hasWhere) { + hasWhere = true; sb.append(" where "); } else { sb.append("and "); @@ -387,7 +405,23 @@ public class CQueryBuilder implements Constants { sb.append(dbFilterMany); } - + List asOfTableAlias = query.getAsOfTableAlias(); + if (asOfTableAlias != null) { + // append the effective date predicates for each table alias + // that maps to a @History entity involved in this query + if (!hasWhere) { + sb.append(" where "); + } else { + sb.append("and "); + } + for (int i = 0; i < asOfTableAlias.size(); i++) { + if (i > 0) { + sb.append(" and "); + } + sb.append(dbPlatform.getAsOfPredicate(asOfTableAlias.get(i), asOfSysPeriod)); + } + } + if (dbOrderBy != null) { sb.append(" order by ").append(dbOrderBy); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java index 1c671e317..771724197 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import java.sql.SQLException; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,9 +32,9 @@ public class CQueryEngine { private final CQueryBuilder queryBuilder; - public CQueryEngine(DatabasePlatform dbPlatform, Binder binder) { + public CQueryEngine(DatabasePlatform dbPlatform, Binder binder, Map asOfTableMapping, String asOfSysPeriod) { this.forwardOnlyHintOnFindIterate = dbPlatform.isForwardOnlyHintOnFindIterate(); - this.queryBuilder = new CQueryBuilder(dbPlatform, binder); + this.queryBuilder = new CQueryBuilder(dbPlatform, binder, asOfTableMapping, asOfSysPeriod); } public CQuery buildQuery(OrmQueryRequest request) { 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 21853340f..2c0a6bf7b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java @@ -1,8 +1,10 @@ package com.avaje.ebeaninternal.server.query; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import com.avaje.ebean.RawSql; @@ -174,6 +176,17 @@ public class CQueryPredicates { } } + List historyTableAlias = query.getAsOfTableAlias(); + if (historyTableAlias != null) { + // bind the asAt value for each table alias + // 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(); i++) { + binder.bindObject(dataBind, asOf); + } + } + if (havingNamedParams != null) { // bind named parameters in having... bindLog.append(" havingNamed "); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java index ecc1118a8..9008677ce 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -1,14 +1,15 @@ package com.avaje.ebeaninternal.server.query; -import java.util.ArrayList; -import java.util.HashSet; - import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.deploy.DbSqlContext; import com.avaje.ebeaninternal.server.deploy.TableJoinColumn; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.util.ArrayStack; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; + public class DefaultDbSqlContext implements DbSqlContext { private static final String COMMA = ", "; @@ -45,25 +46,22 @@ public class DefaultDbSqlContext implements DbSqlContext { private ArrayList encryptedProps; - /** - * Construct for FROM clause (no column alias used). - */ - public DefaultDbSqlContext(SqlTreeAlias alias, String tableAliasPlaceHolder) { - this.tableAliasPlaceHolder = tableAliasPlaceHolder; - this.columnAliasPrefix = null; - this.useColumnAlias = false; - this.alias = alias; - } + private final Map asOfTableMap; + + private final boolean asOfQuery; /** * Construct for SELECT clause (with column alias settings). */ public DefaultDbSqlContext(SqlTreeAlias alias, String tableAliasPlaceHolder, - String columnAliasPrefix, boolean alwaysUseColumnAlias) { + String columnAliasPrefix, boolean alwaysUseColumnAlias, Map asOfTableMap) { + this.alias = alias; this.tableAliasPlaceHolder = tableAliasPlaceHolder; this.columnAliasPrefix = columnAliasPrefix; this.useColumnAlias = alwaysUseColumnAlias; + this.asOfTableMap = asOfTableMap; + this.asOfQuery = (asOfTableMap != null); } public void addEncryptedProp(BeanProperty p) { @@ -108,7 +106,21 @@ public class DefaultDbSqlContext implements DbSqlContext { sb.append(" "); sb.append(type); - sb.append(" ").append(table).append(" "); + if (!asOfQuery) { + sb.append(" ").append(table).append(" "); + + } else { + // check if there is an associated history table and if so + // use the unionAll view - we expect an additional predicate to match + String withHistoryTable = asOfTableMap.get(table); + if (withHistoryTable != null) { + // there is an associated history table and view so use that + sb.append(" ").append(withHistoryTable).append(" "); + + } else { + sb.append(" ").append(table).append(" "); + } + } sb.append(a2); sb.append(" on "); @@ -222,7 +234,7 @@ public class DefaultDbSqlContext implements DbSqlContext { public void appendColumn(String tableAlias, String column) { sb.append(COMMA); - if (column.indexOf("${}") > -1) { + if (column.contains("${}")) { // support DB functions such as lower() etc // with the use of secondary columns String x = StringHelper.replaceString(column, "${}", tableAlias); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java index 038ee8c33..82fb447d3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.el.ElPropertyValue; @@ -80,6 +81,13 @@ public class SqlTree { this.includes = null; } + /** + * Recurse through the tree adding an table alias' for @History entity beans. + */ + public void addAsOfTableAlias(SpiQuery query) { + rootNode.addAsOfTableAlias(query); + } + /** * Build a select expression chain for RawSql. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java index a5dfc2af7..20322ea98 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java @@ -98,7 +98,7 @@ public class SqlTreeBuilder { * to the root node. */ public SqlTreeBuilder(String tableAliasPlaceHolder, String columnAliasPrefix, - OrmQueryRequest request, CQueryPredicates predicates) { + OrmQueryRequest request, CQueryPredicates predicates, Map asOfTables) { this.rawSql = false; this.rawNoId = false; @@ -112,7 +112,7 @@ public class SqlTreeBuilder { this.predicates = predicates; this.alias = new SqlTreeAlias(request.getQuery().getAlias()==null?request.getBeanDescriptor().getBaseTableAlias():request.getQuery().getAlias()); - this.ctx = new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery); + this.ctx = new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery, asOfTables); } /** @@ -265,7 +265,8 @@ public class SqlTreeBuilder { // Optional many property for lazy loading query BeanPropertyAssocMany lazyLoadMany = (query == null) ? null : query.getLazyLoadForParentsProperty(); boolean withId = !rawNoId && !subQuery && (query == null || !query.isDistinct()); - return new SqlTreeNodeRoot(desc, props, myList, withId, includeJoin, lazyLoadMany); + + return new SqlTreeNodeRoot(desc, props, myList, withId, includeJoin, lazyLoadMany, SpiQuery.TemporalMode.of(query)); } else if (prop instanceof BeanPropertyAssocMany) { return new SqlTreeNodeManyRoot(prefix, (BeanPropertyAssocMany) prop, props, myList); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java index 0a4864eff..01b50c498 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.DbReadContext; import com.avaje.ebeaninternal.server.deploy.DbSqlContext; @@ -32,6 +33,11 @@ public interface SqlTreeNode { */ void appendWhere(DbSqlContext ctx); + /** + * Recurse through the tree adding an table alias' for @History entity beans. + */ + void addAsOfTableAlias(SpiQuery query); + /** * Load the appropriate information from the SqlSelectReader. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java index 3ad44adf3..70038ce33 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -9,6 +9,7 @@ import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.bean.EntityBeanIntercept; import com.avaje.ebean.bean.PersistenceContext; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.api.SpiQuery.Mode; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanProperty; @@ -70,25 +71,36 @@ public class SqlTreeNodeBean implements SqlTreeNode { protected final BeanPropertyAssocMany lazyLoadParent; + protected final SpiQuery.TemporalMode temporalMode; + private final IdBinder lazyLoadParentIdBinder; + protected String baseTableAlias; + + /** + * Table alias set if this bean node includes a join to a intersection + * table and that table has history support. + */ + protected String intersectionAsOfTableAlias; + public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, SqlTreeProperties props, List myChildren, boolean withId) { - this(prefix, beanProp, beanProp.getTargetDescriptor(), props, myChildren, withId, null); + this(prefix, beanProp, beanProp.getTargetDescriptor(), props, myChildren, withId, null, SpiQuery.TemporalMode.CURRENT); } /** * Create with the appropriate node. */ public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, BeanDescriptor desc, - SqlTreeProperties props, List myChildren, boolean withId, BeanPropertyAssocMany lazyLoadParent) { + SqlTreeProperties props, List myChildren, boolean withId, BeanPropertyAssocMany lazyLoadParent, SpiQuery.TemporalMode temporalMode) { this.lazyLoadParent = lazyLoadParent; this.lazyLoadParentIdBinder = (lazyLoadParent == null) ? null : lazyLoadParent.getBeanDescriptor().getIdBinder(); this.prefix = prefix; this.nodeBeanProp = beanProp; this.desc = desc; + this.temporalMode = temporalMode; this.inheritInfo = desc.getInheritInfo(); this.extraWhere = (beanProp == null) ? null : beanProp.getExtraWhere(); @@ -441,6 +453,8 @@ public class SqlTreeNodeBean implements SqlTreeNode { ctx.pushJoin(prefix); ctx.pushTableAlias(prefix); + baseTableAlias = ctx.getTableAlias(prefix); + // join and return SqlJoinType to use for child joins joinType = appendFromBaseTable(ctx, joinType); @@ -457,6 +471,21 @@ public class SqlTreeNodeBean implements SqlTreeNode { ctx.popJoin(); } + public void addAsOfTableAlias(SpiQuery query) { + // if history on this bean type add it's alias + // for each alias we add an effect date predicate + if (desc.isHistorySupport()) { + query.addAsOfTableAlias(baseTableAlias); + } + if (intersectionAsOfTableAlias != null) { + // adds the 'as of' predicate for this intersection table + query.addAsOfTableAlias(intersectionAsOfTableAlias); + } + for (int i = 0; i < children.length; i++) { + children[i].addAsOfTableAlias(query); + } + } + /** * Join to base table for this node. This includes a join to the intersection * table if this is a ManyToMany node. @@ -472,8 +501,12 @@ public class SqlTreeNodeBean implements SqlTreeNode { String parentAlias = ctx.getTableAlias(split[0]); String alias2 = alias + "z_"; + // adding the additional join to the intersection table TableJoin manyToManyJoin = manyProp.getIntersectionTableJoin(); manyToManyJoin.addJoin(joinType, parentAlias, alias2, ctx); + if (!manyProp.isExcludedFromHistory()) { + intersectionAsOfTableAlias = alias2; + } return nodeBeanProp.addJoin(joinType, alias2, alias, ctx); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java index 247d843bf..77669f4c8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.deploy.DbReadContext; @@ -40,6 +41,11 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode { // nothing to add } + @Override + public void addAsOfTableAlias(SpiQuery query) { + // nothing to do here + } + /** * Return true if the extra join is a many join. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java index ca19f16bd..b2962ca52 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java @@ -13,7 +13,7 @@ public final class SqlTreeNodeManyRoot extends SqlTreeNodeBean { final BeanPropertyAssocMany manyProp; public SqlTreeNodeManyRoot(String prefix, BeanPropertyAssocMany prop, SqlTreeProperties props, List myList) { - super(prefix, prop, prop.getTargetDescriptor(), props, myList, true, null); + super(prefix, prop, prop.getTargetDescriptor(), props, myList, true, null, null); this.manyProp = prop; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java index ad60eec51..6e21b0599 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne; @@ -37,6 +38,11 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { this.parentPrefix = split[0]; } + @Override + public void addAsOfTableAlias(SpiQuery query) { + // do nothing here ... + } + /** * Append to the FROM clause for this node. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java index 9ed916096..c9e10ca18 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.deploy.DbSqlContext; @@ -17,13 +18,14 @@ public final class SqlTreeNodeRoot extends SqlTreeNodeBean { /** * Specify for SqlSelect to include an Id property or not. */ - public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId, TableJoin includeJoin, BeanPropertyAssocMany many) { - super(null, null, desc, props, myList, withId, many); + public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId, + TableJoin includeJoin, BeanPropertyAssocMany many, SpiQuery.TemporalMode temporalMode) { + super(null, null, desc, props, myList, withId, many, temporalMode); this.includeJoin = includeJoin; } public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId) { - super(null, null, desc, props, myList, withId, null); + super(null, null, desc, props, myList, withId, null, null); this.includeJoin = null; } @@ -33,11 +35,11 @@ public final class SqlTreeNodeRoot extends SqlTreeNodeBean { @Override public SqlJoinType appendFromBaseTable(DbSqlContext ctx, SqlJoinType joinType) { - ctx.append(desc.getBaseTable()); - ctx.append(" ").append(ctx.getTableAlias(null)); + ctx.append(desc.getBaseTable(temporalMode)); + ctx.append(" ").append(baseTableAlias); if (includeJoin != null) { - String a1 = ctx.getTableAlias(null); + String a1 = baseTableAlias; String a2 = "int_"; // unique alias for intersection join includeJoin.addJoin(joinType, a1, a2, ctx); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 57479db7d..462ab6f95 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.querydefn; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -12,7 +13,6 @@ import com.avaje.ebean.*; import com.avaje.ebean.OrderBy.Property; import com.avaje.ebean.bean.BeanCollectionTouched; import com.avaje.ebean.bean.CallStack; -import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.bean.ObjectGraphNode; import com.avaje.ebean.bean.ObjectGraphOrigin; import com.avaje.ebean.bean.PersistenceContext; @@ -153,6 +153,16 @@ public class DefaultOrmQuery implements SpiQuery { private DefaultExpressionList havingExpressions; + /** + * The list of table alias associated with @History entity beans. + */ + private List asOfTableAlias; + + /** + * Set for flashback style 'as of' query. + */ + private Timestamp asOf; + private int bufferFetchSizeHint; private boolean usageProfiling = true; @@ -258,6 +268,30 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + /** + * This table alias is for a @History entity involved in the query and as + * such we need to add a 'as of predicate' to the query using this alias. + */ + public void addAsOfTableAlias(String tableAlias) { + if (asOfTableAlias == null) { + asOfTableAlias = new ArrayList(); + } + asOfTableAlias.add(tableAlias); + } + + public List getAsOfTableAlias() { + return asOfTableAlias; + } + + public Timestamp getAsOf() { + return asOf; + } + + public DefaultOrmQuery asOf(Timestamp asOfDateTime) { + this.asOf = asOfDateTime; + return this; + } + /** * Set the BeanDescriptor for the root type of this query. */ @@ -581,6 +615,15 @@ public class DefaultOrmQuery implements SpiQuery { return mode; } + @Override + public TemporalMode getTemporalMode() { + return asOf == null ? TemporalMode.CURRENT : TemporalMode.AS_OF; + } + + public boolean isAsOfQuery() { + return asOf != null; + } + public void setMode(Mode mode) { this.mode = mode; } @@ -665,6 +708,7 @@ public class DefaultOrmQuery implements SpiQuery { builder.add(rawWhereClause).add(additionalWhere).add(additionalHaving); builder.add(mapKey); builder.add(id != null); + builder.add(asOf != null); builder.add(rawSql == null ? 0 : rawSql.queryHash()); builder.add(includeTableJoin != null ? includeTableJoin.queryHash() : 0); builder.add(rootTableAlias); @@ -735,6 +779,7 @@ public class DefaultOrmQuery implements SpiQuery { hc = hc * 31 + (whereExpressions == null ? 0 : whereExpressions.queryBindHash()); hc = hc * 31 + (havingExpressions == null ? 0 : havingExpressions.queryBindHash()); hc = hc * 31 + (bindParams == null ? 0 : bindParams.queryBindHash()); + hc = hc * 31 + (asOf == null ? 0 : asOf.hashCode()); return hc; } diff --git a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java index 05c2cdf3f..f1404b205 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.util; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -107,6 +108,11 @@ public class DefaultExpressionList implements SpiExpressionList { return query; } + @Override + public Query asOf(Timestamp asOf) { + return query.asOf(asOf); + } + @Override public ExpressionList where() { return query.where();