mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#347 - @History support with query.asOf(Timestamp)
This commit is contained in:
@@ -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<T> extends Serializable {
|
||||
*/
|
||||
Query<T> apply(PathProperties pathProperties);
|
||||
|
||||
/**
|
||||
* Perform an 'As of' query using history tables to return the object graph
|
||||
* as of a time in the past.
|
||||
* <p>
|
||||
* To perform this query the DB must have underlying history tables.
|
||||
* </p>
|
||||
*
|
||||
* @param asOf the date time in the past at which you want to view the data
|
||||
*/
|
||||
Query<T> asOf(Timestamp asOf);
|
||||
|
||||
/**
|
||||
* Execute the query iterating over the results.
|
||||
*
|
||||
|
||||
@@ -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<T> extends Serializable {
|
||||
*/
|
||||
Query<T> setRawSql(RawSql rawSql);
|
||||
|
||||
/**
|
||||
* Perform an 'As of' query using history tables to return the object graph
|
||||
* as of a time in the past.
|
||||
* <p>
|
||||
* To perform this query the DB must have underlying history tables.
|
||||
* </p>
|
||||
*
|
||||
* @param asOf the date time in the past at which you want to view the data
|
||||
*/
|
||||
Query<T> asOf(Timestamp asOf);
|
||||
|
||||
/**
|
||||
* Cancel the query execution if supported by the underlying database and
|
||||
* driver.
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* In Postgres for example this means there is an associated history table which is
|
||||
* typically automatically populated via database triggers.
|
||||
* </p>
|
||||
*/
|
||||
@Target({ ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface History {
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* When placed on a ManyToMany this means that the intersection table does not have history
|
||||
* support.
|
||||
* </p>
|
||||
*/
|
||||
@Target({ ElementType.FIELD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface HistoryExclude {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -60,12 +60,24 @@ public class PostgresPlatform extends DatabasePlatform {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return the 'as of' predicate for a given table alias.
|
||||
* <p>
|
||||
* Each @History entity involved in the query has this predicate added using the related table alias.
|
||||
* </p>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<T> extends Query<T> {
|
||||
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.
|
||||
* <p>
|
||||
@@ -115,6 +140,31 @@ public interface SpiQuery<T> extends Query<T> {
|
||||
*/
|
||||
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<String> getAsOfTableAlias();
|
||||
|
||||
/**
|
||||
* Return a listener that wants to be notified when the bean collection is
|
||||
* first used.
|
||||
|
||||
@@ -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<String, String> 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()) {
|
||||
|
||||
@@ -112,6 +112,8 @@ public class BeanDescriptor<T> 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<T> 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<T> implements MetaBeanInfo {
|
||||
* These properties need to be initialised prior to the association properties
|
||||
* as they are used to get the imported and exported properties.
|
||||
* </p>
|
||||
* @param withHistoryTables map populated if @History is supported on this entity bean
|
||||
*/
|
||||
public void initialiseId() {
|
||||
public void initialiseId(Map<String, String> 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<T> 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<String, String> 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<T> implements MetaBeanInfo {
|
||||
}
|
||||
|
||||
public void cacheBeanPut(T bean) {
|
||||
cacheBeanPutData((EntityBean)bean);
|
||||
cacheBeanPutData((EntityBean) bean);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1628,6 +1654,24 @@ public class BeanDescriptor<T> 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.
|
||||
*/
|
||||
|
||||
@@ -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<String,String> asOfTableMap = new HashMap<String, String>();
|
||||
|
||||
/**
|
||||
* 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<String,String> 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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,11 @@ public class DeployBeanDescriptor<T> {
|
||||
* 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<T> {
|
||||
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<T> {
|
||||
return baseTable;
|
||||
}
|
||||
|
||||
public String getBaseTableAsOf() {
|
||||
return baseTableAsOf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base table with full structure.
|
||||
*/
|
||||
@@ -413,12 +436,12 @@ public class DeployBeanDescriptor<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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){
|
||||
|
||||
@@ -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<T> implements Junction<T>, SpiExpression, Expr
|
||||
throw new RuntimeException("filterMany not allowed on Junction expression list");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> asOf(Timestamp asOf) {
|
||||
return exprList.asOf(asOf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> apply(PathProperties pathProperties) {
|
||||
return exprList.apply(pathProperties);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<String, DLoadManyContext> manyMap = new HashMap<String, DLoadManyContext>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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<String,String> asOfTableMapping;
|
||||
|
||||
private final String asOfSysPeriod;
|
||||
|
||||
private DatabasePlatform dbPlatform;
|
||||
|
||||
/**
|
||||
* Create the SqlGenSelect.
|
||||
*/
|
||||
public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder) {
|
||||
public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, Map<String,String> 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<String,String> 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<String,String> 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<T> query = request.getQuery();
|
||||
|
||||
Map<String,String> 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.
|
||||
* </p>
|
||||
*/
|
||||
private SqlTree createSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates) {
|
||||
private SqlTree createSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates, Map<String,String> 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<String> 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);
|
||||
}
|
||||
|
||||
@@ -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<String,String> asOfTableMapping, String asOfSysPeriod) {
|
||||
this.forwardOnlyHintOnFindIterate = dbPlatform.isForwardOnlyHintOnFindIterate();
|
||||
this.queryBuilder = new CQueryBuilder(dbPlatform, binder);
|
||||
this.queryBuilder = new CQueryBuilder(dbPlatform, binder, asOfTableMapping, asOfSysPeriod);
|
||||
}
|
||||
|
||||
public <T> CQuery<T> buildQuery(OrmQueryRequest<T> request) {
|
||||
|
||||
@@ -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<String> 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 ");
|
||||
|
||||
@@ -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<BeanProperty> 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<String,String> 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<String,String> 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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<String,String> 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);
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
|
||||
@@ -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<SqlTreeNode> 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<SqlTreeNode> myChildren, boolean withId, BeanPropertyAssocMany<?> lazyLoadParent) {
|
||||
SqlTreeProperties props, List<SqlTreeNode> 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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
|
||||
@@ -13,7 +13,7 @@ public final class SqlTreeNodeManyRoot extends SqlTreeNodeBean {
|
||||
final BeanPropertyAssocMany<?> manyProp;
|
||||
|
||||
public SqlTreeNodeManyRoot(String prefix, BeanPropertyAssocMany<?> prop, SqlTreeProperties props, List<SqlTreeNode> myList) {
|
||||
super(prefix, prop, prop.getTargetDescriptor(), props, myList, true, null);
|
||||
super(prefix, prop, prop.getTargetDescriptor(), props, myList, true, null, null);
|
||||
this.manyProp = prop;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<SqlTreeNode> myList, boolean withId, TableJoin includeJoin, BeanPropertyAssocMany<?> many) {
|
||||
super(null, null, desc, props, myList, withId, many);
|
||||
public SqlTreeNodeRoot(BeanDescriptor<?> desc, SqlTreeProperties props, List<SqlTreeNode> 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<SqlTreeNode> 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);
|
||||
}
|
||||
|
||||
@@ -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<T> implements SpiQuery<T> {
|
||||
|
||||
private DefaultExpressionList<T> havingExpressions;
|
||||
|
||||
/**
|
||||
* The list of table alias associated with @History entity beans.
|
||||
*/
|
||||
private List<String> 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<T> implements SpiQuery<T> {
|
||||
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<String>();
|
||||
}
|
||||
asOfTableAlias.add(tableAlias);
|
||||
}
|
||||
|
||||
public List<String> getAsOfTableAlias() {
|
||||
return asOfTableAlias;
|
||||
}
|
||||
|
||||
public Timestamp getAsOf() {
|
||||
return asOf;
|
||||
}
|
||||
|
||||
public DefaultOrmQuery<T> 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<T> implements SpiQuery<T> {
|
||||
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<T> implements SpiQuery<T> {
|
||||
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<T> implements SpiQuery<T> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T> implements SpiExpressionList<T> {
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> asOf(Timestamp asOf) {
|
||||
return query.asOf(asOf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> where() {
|
||||
return query.where();
|
||||
|
||||
Reference in New Issue
Block a user