From 1ec2f74df928a123f904da880b3a08bcc2e917a6 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Fri, 24 Jul 2015 16:20:15 +1200 Subject: [PATCH] #350 - ENH: Add @History findVersions() query ... returning historic versions for a given bean using history view --- .../java/com/avaje/ebean/EbeanServer.java | 9 +++ .../java/com/avaje/ebean/ExpressionList.java | 9 +++ src/main/java/com/avaje/ebean/Query.java | 9 +++ src/main/java/com/avaje/ebean/Version.java | 81 +++++++++++++++++++ .../config/dbplatform/DatabasePlatform.java | 30 ++++--- .../config/dbplatform/DbHistorySupport.java | 33 ++++++++ .../config/dbplatform/Postgres8Platform.java | 20 +---- .../dbplatform/PostgresHistorySupport.java | 33 ++++++++ .../config/dbplatform/PostgresPlatform.java | 16 +--- .../server/core/DefaultServer.java | 13 +++ .../server/core/OrmQueryEngine.java | 48 ++++++----- .../server/core/OrmQueryRequest.java | 4 + .../server/core/SpiOrmQueryRequest.java | 6 ++ .../server/deploy/DbSqlContext.java | 6 ++ .../server/expression/JunctionExpression.java | 5 ++ .../ebeaninternal/server/query/CQuery.java | 27 +++++++ .../server/query/CQueryBuilder.java | 29 ++++--- .../server/query/CQueryEngine.java | 31 +++++++ .../server/query/CQueryHistorySupport.java | 47 +++++++++++ .../server/query/DefaultDbSqlContext.java | 53 +++++++----- .../server/query/DefaultOrmQueryEngine.java | 9 +++ .../server/query/SqlTreeBuilder.java | 4 +- .../server/query/SqlTreeNode.java | 5 ++ .../server/query/SqlTreeNodeBean.java | 49 ++++++++--- .../server/query/SqlTreeNodeExtraJoin.java | 8 ++ .../query/SqlTreeNodeManyWhereJoin.java | 6 ++ .../server/querydefn/DefaultOrmQuery.java | 11 ++- .../util/DefaultExpressionList.java | 5 ++ .../ebeaninternal/api/TDSpiEbeanServer.java | 5 ++ 29 files changed, 500 insertions(+), 111 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/Version.java create mode 100644 src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java create mode 100644 src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index f6daf95e7..fb2a7354a 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -904,6 +904,15 @@ public interface EbeanServer { */ void findEachWhile(Query query, QueryEachWhileConsumer consumer, Transaction transaction); + /** + * Return versions of a @History entity bean. + *

+ * Generally this query is expected to be a find by id or unique predicates query. + * It will execute the query against the history returning the versions of the bean. + *

+ */ + List> findVersions(Query query, Transaction transaction); + /** * Execute a query returning a list of beans. *

diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 4117f50cb..ac523960c 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -235,6 +235,15 @@ public interface ExpressionList extends Serializable { */ PagedList findPagedList(int pageIndex, int pageSize); + /** + * Return versions of a @History entity bean. + *

+ * Generally this query is expected to be a find by id or unique predicates query. + * It will execute the query against the history returning the versions of the bean. + *

+ */ + List> findVersions(); + /** * Add some filter predicate expressions to the many property. */ diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index 03f4fd22e..8bab2c0cc 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -720,6 +720,15 @@ public interface Query extends Serializable { @Nullable T findUnique(); + /** + * Return versions of a @History entity bean. + *

+ * Generally this query is expected to be a find by id or unique predicates query. + * It will execute the query against the history returning the versions of the bean. + *

+ */ + List> findVersions(); + /** * Return the count of entities this query should return. *

diff --git a/src/main/java/com/avaje/ebean/Version.java b/src/main/java/com/avaje/ebean/Version.java new file mode 100644 index 000000000..cca628ad5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Version.java @@ -0,0 +1,81 @@ +package com.avaje.ebean; + +import java.sql.Timestamp; + +/** + * Wraps a version of a @History bean. + */ +public class Version { + + /** + * The version of the bean. + */ + T bean; + + /** + * The effective start date time of this version. + */ + Timestamp start; + + /** + * The effective end date time of this version. + */ + Timestamp end; + + /** + * Construct with bean and an effective date time range. + */ + public Version(T bean, Timestamp start, Timestamp end) { + this.bean = bean; + this.start = start; + this.end = end; + } + + /** + * Default constructor - useful for JSON tools such as Jackson. + */ + public Version() { + } + + /** + * Return the bean instance for this version. + */ + public T getBean() { + return bean; + } + + /** + * Set the bean instance for this version. + */ + public void setBean(T bean) { + this.bean = bean; + } + + /** + * Return the effective start date time of this version. + */ + public Timestamp getStart() { + return start; + } + + /** + * Set the effective start date time of this version. + */ + public void setStart(Timestamp start) { + this.start = start; + } + + /** + * Return the effective end date time of this version. + */ + public Timestamp getEnd() { + return end; + } + + /** + * Set the effective end date time of this version. + */ + public void setEnd(Timestamp end) { + this.end = end; + } +} 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 eb128c42a..c73b9bd94 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -80,6 +80,11 @@ public class DatabasePlatform { */ protected DbIdentity dbIdentity = new DbIdentity(); + /** + * The history support for this database platform. + */ + protected DbHistorySupport historySupport; + /** * The JDBC type to map booleans to (by default). */ @@ -215,6 +220,20 @@ public class DatabasePlatform { this.dbEncrypt = dbEncrypt; } + /** + * Return the history support for this database platform. + */ + public DbHistorySupport getHistorySupport() { + return historySupport; + } + + /** + * Set the history support for this database platform. + */ + public void setHistorySupport(DbHistorySupport historySupport) { + this.historySupport = historySupport; + } + /** * Return the mapping of JDBC to DB types. * @@ -442,17 +461,6 @@ public class DatabasePlatform { return disallowBatchOnCascade; } - /** - * Return the 'as of' predicate added for the given table alias. - * - * @param asOfTableAlias The table alias this predicate is added for - * @param asOfSysPeriod The name of the 'sys_period' column used for effective date time range. - * @return The predicate containing a single ? bind parameter which will be bound to the 'as at' timestamp value - */ - 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/DbHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java new file mode 100644 index 000000000..c61cd0bc6 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbHistorySupport.java @@ -0,0 +1,33 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * History support for the database platform. + */ +public interface DbHistorySupport { + + /** + * Return the 'as of' predicate added for the given table alias. + * + * @param tableAlias The table alias this predicate is added for + * @param sysPeriod The name of the 'sys_period' column used for effective date time range. + * @return The predicate containing a single ? bind parameter which will be bound to the 'as at' timestamp value + */ + String getAsOfPredicate(String tableAlias, String sysPeriod); + + /** + * 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 + */ + String getSysPeriodLower(String tableAlias, String sysPeriod); + + /** + * 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 + */ + String getSysPeriodUpper(String tableAlias, String sysPeriod); + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Postgres8Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/Postgres8Platform.java index 740c73c55..3643bc004 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/Postgres8Platform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Postgres8Platform.java @@ -21,6 +21,7 @@ public class Postgres8Platform extends DatabasePlatform { this.clobDbType = Types.VARCHAR; this.dbEncrypt = new PostgresDbEncrypt(); + this.historySupport = new PostgresHistorySupport(); this.dbIdentity.setSupportsGetGeneratedKeys(false); this.dbIdentity.setIdType(IdType.SEQUENCE); @@ -47,30 +48,13 @@ public class Postgres8Platform extends DatabasePlatform { dbDdlSyntax.setDropTableCascade("cascade"); dbDdlSyntax.setDropIfExists("if exists"); - - } - - /** - * 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) { - - // for Postgres we are using the 'timestamp with timezone range' data type - // as our sys_period column so hence the predicate below - 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/ebean/config/dbplatform/PostgresHistorySupport.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java new file mode 100644 index 000000000..1f6e7669e --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresHistorySupport.java @@ -0,0 +1,33 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Postgres support for history features. + */ +public class PostgresHistorySupport implements DbHistorySupport { + + /** + * 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. + *

+ */ + @Override + public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) { + + // for Postgres we are using the 'timestamp with timezone range' data type + // as our sys_period column so hence the predicate below + StringBuilder sb = new StringBuilder(40); + sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append(" @> ?::timestamptz"); + return sb.toString(); + } + + @Override + public String getSysPeriodLower(String tableAlias, String sysPeriod) { + return "lower("+tableAlias+"."+sysPeriod+")"; + } + + @Override + public String getSysPeriodUpper(String tableAlias, String sysPeriod) { + return "upper("+tableAlias+"."+sysPeriod+")"; + } +} 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 950322a35..15b4d9b46 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -27,6 +27,7 @@ public class PostgresPlatform extends DatabasePlatform { this.clobDbType = Types.VARCHAR; this.dbEncrypt = new PostgresDbEncrypt(); + this.historySupport = new PostgresHistorySupport(); // Use Identity and getGeneratedKeys this.dbIdentity.setIdType(IdType.IDENTITY); @@ -57,23 +58,8 @@ public class PostgresPlatform extends DatabasePlatform { dbDdlSyntax.setDropTableCascade("cascade"); dbDdlSyntax.setDropIfExists("if exists"); - } - /** - * 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) { - - // for Postgres we are using the 'timestamp with timezone range' data type - // as our sys_period column so hence the predicate below - StringBuilder sb = new StringBuilder(40); - sb.append(asOfTableAlias).append(".").append(asOfSysPeriod).append(" @> ?::timestamptz"); - return sb.toString(); - } /** * Create a Postgres specific sequence IdGenerator. diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index f79c6fe79..3bdf44108 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1355,6 +1355,19 @@ public final class DefaultServer implements SpiEbeanServer { } } + @Override + public List> findVersions(Query query, Transaction transaction) { + + SpiOrmQueryRequest request = createQueryRequest(Type.LIST, query, transaction); + try { + request.initTransIfRequired(); + return request.findVersions(); + + } finally { + request.endTransIfRequired(); + } + } + @SuppressWarnings("unchecked") public List findList(Query query, Transaction t) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java index aa6f3b0d2..425685f3e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryEngine.java @@ -1,38 +1,42 @@ package com.avaje.ebeaninternal.server.core; import com.avaje.ebean.QueryIterator; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebeaninternal.api.BeanIdList; +import java.util.List; + /** * The Object Relational query execution API. */ public interface OrmQueryEngine { - /** - * Execute the 'find by id' query returning a single bean. - */ - public T findId(OrmQueryRequest request); + /** + * Execute the 'find by id' query returning a single bean. + */ + T findId(OrmQueryRequest request); - /** - * Execute the findList, findSet, findMap query returning an appropriate BeanCollection. - */ - public BeanCollection findMany(OrmQueryRequest request); + /** + * Execute the findList, findSet, findMap query returning an appropriate BeanCollection. + */ + BeanCollection findMany(OrmQueryRequest request); - /** - * Execute the query using a QueryIterator. - */ - public QueryIterator findIterate(OrmQueryRequest request); - - /** - * Execute the row count query. - */ - public int findRowCount(OrmQueryRequest request); - - /** - * Execute the find id's query. - */ - public BeanIdList findIds(OrmQueryRequest request); + List> findVersions(OrmQueryRequest request); + /** + * Execute the query using a QueryIterator. + */ + QueryIterator findIterate(OrmQueryRequest request); + + /** + * Execute the row count query. + */ + int findRowCount(OrmQueryRequest request); + + /** + * Execute the find id's query. + */ + BeanIdList findIds(OrmQueryRequest request); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java index 012c53c80..09a9a5dc2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java @@ -271,6 +271,10 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe return (List) queryEngine.findMany(this); } + public List> findVersions() { + return queryEngine.findVersions(this); + } + /** * Execute the query as findSet. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java index 55a271d1b..1204b3335 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -3,6 +3,7 @@ package com.avaje.ebeaninternal.server.core; import com.avaje.ebean.QueryEachConsumer; import com.avaje.ebean.QueryEachWhileConsumer; import com.avaje.ebean.QueryIterator; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; @@ -76,6 +77,11 @@ public interface SpiOrmQueryRequest { */ QueryIterator findIterate(); + /** + * Execute the finVersions() query. + */ + List> findVersions(); + /** * Execute the query as findList. */ 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 36b6d366c..2542c7c7b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java @@ -114,4 +114,10 @@ public interface DbSqlContext { String getRelativePrefix(String propName); + /** + * Append the lower and upper bound columns into the select clause + * for findVersions() queries. + */ + void appendHistorySysPeriod(); + } 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 19a0fae3c..58b7965ae 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -223,6 +223,11 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr return exprList.asOf(asOf); } + @Override + public List> findVersions() { + return exprList.findVersions(); + } + @Override public Query apply(PathProperties pathProperties) { return exprList.apply(pathProperties); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java index d1c49b734..222dd1955 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.QueryIterator; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.*; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.api.SpiQuery.Mode; @@ -22,6 +23,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -479,6 +482,30 @@ public class CQuery implements DbReadContext, CancelableQuery { } } + /** + * Read version beans and their effective dates. + */ + public List> readVersions() throws SQLException { + + List> versionList = new ArrayList>(); + + Version version; + while ((version = readNextVersion()) != null) { + versionList.add(version); + } + + updateExecutionStatistics(); + return versionList; + } + + private Version readNextVersion() throws SQLException { + + if (moveToNextRow()) { + return rootNode.loadVersion(this); + } + return null; + } + public BeanCollection readCollection() throws SQLException { while (hasNext()) { 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 938bd3c10..2c7b7a079 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -5,6 +5,7 @@ import com.avaje.ebean.RawSql.ColumnMapping; import com.avaje.ebean.RawSql.ColumnMapping.Column; import com.avaje.ebean.RawSqlBuilder; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.config.dbplatform.DbHistorySupport; import com.avaje.ebean.config.dbplatform.SqlLimitRequest; import com.avaje.ebean.config.dbplatform.SqlLimitResponse; import com.avaje.ebean.config.dbplatform.SqlLimiter; @@ -48,8 +49,10 @@ public class CQueryBuilder implements Constants { private final Map asOfTableMapping; private final String asOfSysPeriod; + private final DbHistorySupport dbHistorySupport; + private final CQueryHistorySupport historySupport; - private DatabasePlatform dbPlatform; + private final DatabasePlatform dbPlatform; /** * Create the SqlGenSelect. @@ -63,6 +66,8 @@ public class CQueryBuilder implements Constants { this.columnAliasPrefix = dbPlatform.getColumnAliasPrefix(); this.sqlSelectBuilder = new RawSqlSelectClauseBuilder(dbPlatform, binder); + this.dbHistorySupport = dbPlatform.getHistorySupport(); + this.historySupport = new CQueryHistorySupport(dbHistorySupport, asOfTableMapping, asOfSysPeriod); this.sqlLimiter = dbPlatform.getSqlLimiter(); this.rawSqlHandler = new CQueryBuilderRawSql(sqlLimiter, dbPlatform); @@ -111,8 +116,7 @@ public class CQueryBuilder implements Constants { // use RawSql or generated Sql predicates.prepare(true); - Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; - SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); + SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query)); SqlLimitResponse s = buildSql(null, request, predicates, sqlTree); String sql = s.getSql(); @@ -123,6 +127,13 @@ public class CQueryBuilder implements Constants { return new CQueryFetchIds(request, predicates, sql); } + /** + * Return the history support if this query needs it (is a 'as of' type query). + */ + private CQueryHistorySupport getHistorySupport(SpiQuery query) { + return query.getTemporalMode() != SpiQuery.TemporalMode.CURRENT ? historySupport : null; + } + /** * Build the row count query. */ @@ -164,8 +175,7 @@ public class CQueryBuilder implements Constants { predicates.prepare(true); - Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; - SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); + SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query)); SqlLimitResponse s = buildSql(sqlSelect, request, predicates, sqlTree); String sql = s.getSql(); if (hasMany || query.isRawSql()) { @@ -215,8 +225,7 @@ public class CQueryBuilder implements Constants { // Build the tree structure that represents the query. SpiQuery query = request.getQuery(); - Map asOfMap = query.isAsOfQuery() ? asOfTableMapping : null; - SqlTree sqlTree = createSqlTree(request, predicates, asOfMap); + SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query)); if (query.isAsOfQuery()) { sqlTree.addAsOfTableAlias(query); } @@ -249,13 +258,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, Map withHistoryTables) { + private SqlTree createSqlTree(OrmQueryRequest request, CQueryPredicates predicates, CQueryHistorySupport historySupport) { if (request.isRawSql()) { return createRawSqlSqlTree(request, predicates); } - return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, withHistoryTables).build(); + return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, historySupport).build(); } private SqlTree createRawSqlSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { @@ -418,7 +427,7 @@ public class CQueryBuilder implements Constants { if (i > 0) { sb.append(" and "); } - sb.append(dbPlatform.getAsOfPredicate(asOfTableAlias.get(i), asOfSysPeriod)); + sb.append(dbHistorySupport.getAsOfPredicate(asOfTableAlias.get(i), asOfSysPeriod)); } } 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 771724197..def49554f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java @@ -1,8 +1,10 @@ package com.avaje.ebeaninternal.server.query; import java.sql.SQLException; +import java.util.List; import java.util.Map; +import com.avaje.ebean.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -154,6 +156,35 @@ public class CQueryEngine { } } + /** + * Execute the find versions query returning version beans. + */ + public List> findVersions(OrmQueryRequest request) { + + CQuery cquery = queryBuilder.buildQuery(request); + try { + cquery.prepareBindExecuteQuery(); + if (request.isLogSql()) { + logSql(cquery); + } + + List> versions = cquery.readVersions(); + if (request.isLogSummary()) { + logFindManySummary(cquery); + } + + return versions; + + } catch (SQLException e) { + throw cquery.createPersistenceException(e); + + } finally { + if (cquery != null) { + cquery.close(); + } + } + } + /** * Find a list/map/set of beans. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java new file mode 100644 index 000000000..436892c65 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryHistorySupport.java @@ -0,0 +1,47 @@ +package com.avaje.ebeaninternal.server.query; + +import com.avaje.ebean.config.dbplatform.DbHistorySupport; + +import java.util.Map; + +/** + * Helper to support history functions. + */ +public class CQueryHistorySupport { + + /** + * The DB specific support. + */ + private final DbHistorySupport dbHistorySupport; + + /** + * The mapping of base tables to their matching 'with history' views. + */ + private final Map asOfTableMap; + + /** + * The sys period column. + */ + private final String asOfSysPeriod; + + public CQueryHistorySupport(DbHistorySupport dbHistorySupport, Map asOfTableMap, String asOfSysPeriod) { + this.dbHistorySupport = dbHistorySupport; + this.asOfTableMap = asOfTableMap; + this.asOfSysPeriod = asOfSysPeriod; + } + + public String getAsOfView(String table) { + return asOfTableMap.get(table); + } + + public String getSysPeriodLower(String tableAlias) { + + return dbHistorySupport.getSysPeriodLower(tableAlias, asOfSysPeriod); + } + + public String getSysPeriodUpper(String tableAlias) { + + return dbHistorySupport.getSysPeriodUpper(tableAlias, asOfSysPeriod); + } + +} 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 9008677ce..906d46872 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -8,7 +8,6 @@ import com.avaje.ebeaninternal.server.util.ArrayStack; import java.util.ArrayList; import java.util.HashSet; -import java.util.Map; public class DefaultDbSqlContext implements DbSqlContext { @@ -46,22 +45,22 @@ public class DefaultDbSqlContext implements DbSqlContext { private ArrayList encryptedProps; - private final Map asOfTableMap; + private final CQueryHistorySupport historySupport; - private final boolean asOfQuery; + private final boolean historyQuery; /** * Construct for SELECT clause (with column alias settings). */ public DefaultDbSqlContext(SqlTreeAlias alias, String tableAliasPlaceHolder, - String columnAliasPrefix, boolean alwaysUseColumnAlias, Map asOfTableMap) { + String columnAliasPrefix, boolean alwaysUseColumnAlias, CQueryHistorySupport historySupport) { this.alias = alias; this.tableAliasPlaceHolder = tableAliasPlaceHolder; this.columnAliasPrefix = columnAliasPrefix; this.useColumnAlias = alwaysUseColumnAlias; - this.asOfTableMap = asOfTableMap; - this.asOfQuery = (asOfTableMap != null); + this.historySupport = historySupport; + this.historyQuery = (historySupport != null); } public void addEncryptedProp(BeanProperty p) { @@ -106,13 +105,14 @@ public class DefaultDbSqlContext implements DbSqlContext { sb.append(" "); sb.append(type); - if (!asOfQuery) { + if (!historyQuery) { 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); + String withHistoryTable = historySupport.getAsOfView(table); + if (withHistoryTable != null) { // there is an associated history table and view so use that sb.append(" ").append(withHistoryTable).append(" "); @@ -227,6 +227,29 @@ public class DefaultDbSqlContext implements DbSqlContext { sb.append(converted); } + @Override + public void appendHistorySysPeriod() { + + String tableAlias = tableAliasStack.peek(); + + sb.append(COMMA); + sb.append(historySupport.getSysPeriodLower(tableAlias)); + appendColumnAlias(); + + sb.append(COMMA); + sb.append(historySupport.getSysPeriodUpper(tableAlias)); + appendColumnAlias(); + } + + private void appendColumnAlias() { + if (useColumnAlias) { + sb.append(" "); + sb.append(columnAliasPrefix); + sb.append(columnIndex); + } + columnIndex++; + } + public void appendColumn(String column) { appendColumn(tableAliasStack.peek(), column); } @@ -244,12 +267,7 @@ public class DefaultDbSqlContext implements DbSqlContext { sb.append(PERIOD); sb.append(column); } - if (useColumnAlias) { - sb.append(" "); - sb.append(columnAliasPrefix); - sb.append(columnIndex); - } - columnIndex++; + appendColumnAlias(); } public String peekTableAlias() { @@ -260,12 +278,7 @@ public class DefaultDbSqlContext implements DbSqlContext { sb.append(COMMA); sb.append(rawcolumnWithTableAlias); - if (useColumnAlias) { - sb.append(" "); - sb.append(columnAliasPrefix); - sb.append(columnIndex); - } - columnIndex++; + appendColumnAlias(); } public int length() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 654715bea..6e410c7ab 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.QueryIterator; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.event.BeanFindController; @@ -12,6 +13,7 @@ import com.avaje.ebeaninternal.server.core.OrmQueryRequest; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import java.util.Collection; +import java.util.List; /** * Main Finder implementation. @@ -65,6 +67,13 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { return queryEngine.findIterate(request); } + @Override + public List> findVersions(OrmQueryRequest request) { + + flushJdbcBatchOnQuery(request); + return queryEngine.findVersions(request); + } + public BeanCollection findMany(OrmQueryRequest request) { flushJdbcBatchOnQuery(request); 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 20322ea98..1944db81b 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, Map asOfTables) { + OrmQueryRequest request, CQueryPredicates predicates, CQueryHistorySupport historySupport) { 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, asOfTables); + this.ctx = new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery, historySupport); } /** 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 01b50c498..dc32662f1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.DbReadContext; @@ -47,4 +48,8 @@ public interface SqlTreeNode { */ EntityBean load(DbReadContext ctx, EntityBean localBean, EntityBean contextBean) throws SQLException; + /** + * Load a version of a @History bean with effective dates. + */ + Version loadVersion(DbReadContext ctx) throws SQLException; } 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 70038ce33..0bd371976 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -1,10 +1,12 @@ package com.avaje.ebeaninternal.server.query; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.BeanCollection; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.bean.EntityBeanIntercept; @@ -73,6 +75,8 @@ public class SqlTreeNodeBean implements SqlTreeNode { protected final SpiQuery.TemporalMode temporalMode; + protected final boolean temporalVersions; + private final IdBinder lazyLoadParentIdBinder; protected String baseTableAlias; @@ -101,6 +105,8 @@ public class SqlTreeNodeBean implements SqlTreeNode { this.nodeBeanProp = beanProp; this.desc = desc; this.temporalMode = temporalMode; + this.temporalVersions = temporalMode == SpiQuery.TemporalMode.VERSIONS; + this.inheritInfo = desc.getInheritInfo(); this.extraWhere = (beanProp == null) ? null : beanProp.getExtraWhere(); @@ -108,7 +114,7 @@ public class SqlTreeNodeBean implements SqlTreeNode { // the bean has an Id property and we want to use it this.readId = withId && (desc.getIdProperty() != null); - this.disableLazyLoad = !readId || desc.isSqlSelectBased(); + this.disableLazyLoad = !readId || desc.isSqlSelectBased() || temporalVersions; this.tableJoins = props.getTableJoins(); @@ -164,6 +170,20 @@ public class SqlTreeNodeBean implements SqlTreeNode { } } + /** + * Read the version bean. + */ + public Version loadVersion(DbReadContext ctx) throws SQLException { + + // read the sys period lower and upper bounds + // these are always the first 2 columns in the resultSet + Timestamp start = ctx.getDataReader().getTimestamp(); + Timestamp end = ctx.getDataReader().getTimestamp(); + T bean = (T)load(ctx, null, null); + + return new Version(bean, start, end); + } + /** * read the properties from the resultSet. */ @@ -206,14 +226,14 @@ public class SqlTreeNodeBean implements SqlTreeNode { Mode queryMode = ctx.getQueryMode(); - PersistenceContext persistenceContext = !readId ? null : ctx.getPersistenceContext(); + PersistenceContext persistenceContext = (!readId || temporalVersions) ? null : ctx.getPersistenceContext(); if (readId) { Object id = localIdBinder.readSet(ctx, localBean); if (id == null) { // bean must be null... localBean = null; - } else { + } else if (!temporalVersions) { // check the PersistenceContext to see if the bean already exists contextBean = (EntityBean)persistenceContext.putIfAbsent(id, localBean); if (contextBean == null) { @@ -278,7 +298,7 @@ public class SqlTreeNodeBean implements SqlTreeNode { if (!lazyLoadMany && localBean != null) { ctx.setCurrentPrefix(prefix, pathMap); - if (readId) { + if (readId && !temporalVersions) { createListProxies(localDesc, ctx, localBean); } localDesc.postLoad(localBean, null); @@ -293,7 +313,11 @@ public class SqlTreeNodeBean implements SqlTreeNode { ebi.setLoaded(); } - if (partialObject) { + if (disableLazyLoad) { + // bean does not have an Id or is SqlSelect based + ebi.setDisableLazyLoad(true); + + } else if (partialObject) { if (readId) { // register for lazy loading ctx.register(null, ebi); @@ -302,11 +326,7 @@ public class SqlTreeNodeBean implements SqlTreeNode { ebi.setFullyLoadedBean(true); } - if (disableLazyLoad) { - // bean does not have an Id or is SqlSelect based - ebi.setDisableLazyLoad(true); - } - if (ctx.isAutoFetchProfiling()) { + if (ctx.isAutoFetchProfiling() && !disableLazyLoad) { // collect autofetch profiling for this bean... ctx.profileBean(ebi, prefix); } @@ -317,7 +337,7 @@ public class SqlTreeNodeBean implements SqlTreeNode { nodeBeanProp.setValue(parentBean, contextBean); } - if (!readId) { + if (!readId || temporalVersions) { // a bean with no Id (never found in context) return localBean; @@ -358,7 +378,12 @@ public class SqlTreeNodeBean implements SqlTreeNode { ctx.pushJoin(prefix); ctx.pushTableAlias(prefix); - + + if (temporalVersions) { + // select sys_period lower and upper columns + ctx.appendHistorySysPeriod(); + } + if (lazyLoadParent != null) { lazyLoadParent.addSelectExported(ctx, prefix); } 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 77669f4c8..f0574d3ce 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc; @@ -126,4 +127,11 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode { return null; } + /** + * Does nothing. + */ + @Override + public Version loadVersion(DbReadContext ctx) throws SQLException { + return null; + } } 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 6e21b0599..396d01891 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import com.avaje.ebean.Version; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc; @@ -98,4 +99,9 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { return null; } + @Override + public Version loadVersion(DbReadContext ctx) throws SQLException { + // nothing to do here + return null; + } } 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 462ab6f95..bd8f7c09b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -163,6 +163,8 @@ public class DefaultOrmQuery implements SpiQuery { */ private Timestamp asOf; + private TemporalMode temporalMode = TemporalMode.CURRENT; + private int bufferFetchSizeHint; private boolean usageProfiling = true; @@ -288,6 +290,7 @@ public class DefaultOrmQuery implements SpiQuery { } public DefaultOrmQuery asOf(Timestamp asOfDateTime) { + this.temporalMode = (asOfDateTime != null) ? TemporalMode.AS_OF : TemporalMode.CURRENT; this.asOf = asOfDateTime; return this; } @@ -617,7 +620,7 @@ public class DefaultOrmQuery implements SpiQuery { @Override public TemporalMode getTemporalMode() { - return asOf == null ? TemporalMode.CURRENT : TemporalMode.AS_OF; + return temporalMode; } public boolean isAsOfQuery() { @@ -952,6 +955,12 @@ public class DefaultOrmQuery implements SpiQuery { server.findEach(this, consumer, null); } + @Override + public List> findVersions() { + this.temporalMode = TemporalMode.VERSIONS; + return server.findVersions(this, null); + } + public QueryIterator findIterate() { return server.findIterate(this, null); } diff --git a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java index f1404b205..71d4d8c52 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/util/DefaultExpressionList.java @@ -113,6 +113,11 @@ public class DefaultExpressionList implements SpiExpressionList { return query.asOf(asOf); } + @Override + public List> findVersions() { + return query.findVersions(); + } + @Override public ExpressionList where() { return query.where(); diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 69d26b4f6..6030c0120 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -153,6 +153,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public List> findVersions(Query query, Transaction transaction) { + return null; + } + @Override public List findIdsWithCopy(Query query, Transaction t) { return null;