#461 - ENH: Add @Draftable support - phase 1

This commit is contained in:
Robin Bygrave
2015-11-19 20:44:25 +13:00
parent 42e7103aa6
commit 7b76e13bce
56 changed files with 944 additions and 214 deletions
@@ -1819,4 +1819,29 @@ public interface EbeanServer {
*/
JsonContext json();
/**
* Publish a single bean given its type and id.
* <p>
* The values are published from the draft to the live bean.
* </p>
*
* @param beanType the type of the entity bean
* @param id the id of the entity bean
* @param transaction the transaction the publish process should use
* @param <T> the type of the entity bean
*/
<T> void publish(Class<T> beanType, Object id, Transaction transaction);
/**
* Publish the beans that match the query.
* <p>
* The values are published from the draft beans to the live beans.
* </p>
*
* @param query the query used to select the draft beans to publish
* @param transaction the transaction the publish process should use
* @param <T> the type of the entity bean
*/
<T> void publish(Query<T> query, Transaction transaction);
}
+5
View File
@@ -297,6 +297,11 @@ public interface Query<T> extends Serializable {
*/
Query<T> asOf(Timestamp asOf);
/**
* Execute the query against the draft set of tables.
*/
Query<T> asDraft();
/**
* Cancel the query execution if supported by the underlying database and
* driver.
@@ -0,0 +1,16 @@
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 boolean property on a @Draftable bean that only exists on the 'draft' table
* and is used to detect when a draft has unpublished changes.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DraftDirty {
}
@@ -0,0 +1,15 @@
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 on a @Draftable bean that only exists on the 'draft' and not the 'live' table.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DraftOnly {
}
@@ -0,0 +1,15 @@
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;
/**
* Used to indicate an entity bean that has 'draftable' support.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Draftable {
}
@@ -0,0 +1,16 @@
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;
/**
* Used to indicate an entity bean that has 'draftable' support but it not a 'root level' bean
* but instead child related to another @Draftable entity bean.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DraftableElement {
}
@@ -77,6 +77,9 @@ public class CurrentModel {
ModelBuildBeanVisitor visitor = new ModelBuildBeanVisitor(context);
VisitAllUsing visit = new VisitAllUsing(visitor, server);
visit.visitAllBeans();
// adjust the foreign keys on the 'draft' tables
context.adjustDraftReferences();
}
return model;
}
@@ -35,6 +35,8 @@ public class MColumn {
*/
private AlterColumn alterColumn;
private boolean draftOnly;
public MColumn(Column column) {
this.name = column.getName();
this.type = column.getType();
@@ -63,6 +65,28 @@ public class MColumn {
this.notnull = notnull;
}
/**
* Return a copy of this column used for creating the associated draft table.
*/
public MColumn copyForDraft() {
MColumn copy = new MColumn(name, type);
copy.draftOnly = draftOnly;
copy.checkConstraint = checkConstraint;
copy.checkConstraintName = checkConstraintName;
copy.defaultValue = defaultValue;
copy.references = references;
copy.foreignKeyName = foreignKeyName;
copy.foreignKeyIndex = foreignKeyIndex;
copy.historyExclude = historyExclude;
copy.notnull = notnull;
copy.primaryKey = primaryKey;
copy.identity = identity;
copy.unique = unique;
copy.uniqueOneToOne = uniqueOneToOne;
return copy;
}
public String getName() {
return name;
}
@@ -174,6 +198,21 @@ public class MColumn {
return uniqueOneToOne;
}
/**
* Set the draftOnly status for this column.
*/
public void setDraftOnly(boolean draftOnly) {
this.draftOnly = draftOnly;
}
/**
* Return the draftOnly status for this column.
*/
public boolean isDraftOnly() {
return draftOnly;
}
public Column createColumn() {
Column c = new Column();
@@ -43,6 +43,17 @@ public class MTable {
*/
private final String name;
/**
* The associated draft table.
*/
private MTable draftTable;
/**
* Marked true for draft tables. These need to have their FK references adjusted
* after all the draft tables have been identified.
*/
private boolean draft;
/**
* Primary key name.
*/
@@ -107,6 +118,27 @@ public class MTable {
*/
private AddColumn addColumn;
/**
* Create a copy of this table structure as a 'draft' table.
*
* Note that both tables contain @DraftOnly MColumns and these are filtered out
* later when creating the CreateTable object.
*/
public MTable createDraftTable() {
draftTable = new MTable(name+"_draft");
draftTable.draft = true;
draftTable.whenCreatedColumn = whenCreatedColumn;
// compoundKeys
// compoundUniqueConstraints
draftTable.identityType = identityType;
for (MColumn col: columns.values()) {
draftTable.addColumn(col.copyForDraft());
}
return draftTable;
}
/**
* Construct for migration.
@@ -164,7 +196,10 @@ public class MTable {
}
for (MColumn column : this.columns.values()) {
createTable.getColumn().add(column.createColumn());
// filter out draftOnly columns from the base table
if (draft || !column.isDraftOnly()) {
createTable.getColumn().add(column.createColumn());
}
}
for (MCompoundForeignKey compoundKey : compoundKeys) {
@@ -271,6 +306,13 @@ public class MTable {
return name;
}
/**
* Return true if this table is a 'Draft' table.
*/
public boolean isDraft() {
return draft;
}
public String getPkName() {
return pkName;
}
@@ -511,4 +553,42 @@ public class MTable {
}
return false;
}
/**
* Adjust the references (FK) if it should relate to a draft table.
*/
public void adjustReferences(ModelContainer modelContainer) {
Collection<MColumn> cols = columns.values();
for (MColumn col : cols) {
String references = col.getReferences();
if (references != null) {
String baseTable = extractBaseTable(references);
MTable refBaseTable = modelContainer.getTable(baseTable);
if (refBaseTable.draftTable != null) {
// change references to another associated 'draft' table
String newReferences = deriveReferences(references, refBaseTable.draftTable.getName());
col.setReferences(newReferences);
}
}
}
}
/**
* Return the base table name from references (table.column).
*/
private String extractBaseTable(String references) {
int lastDot = references.lastIndexOf('.');
return references.substring(0,lastDot);
}
/**
* Return the new references using the given draftTableName.
* (The referenced column is the same as before).
*/
private String deriveReferences(String references, String draftTableName) {
int lastDot = references.lastIndexOf('.');
return draftTableName+"."+references.substring(lastDot+1);
}
}
@@ -12,6 +12,7 @@ import com.avaje.ebean.dbmigration.migration.DropIndex;
import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -38,6 +39,18 @@ public class ModelContainer {
}
/**
* Adjust the FK references on all the draft tables.
*/
public void adjustDraftReferences() {
Collection<MTable> tables = this.tables.values();
for (MTable table : tables) {
if (table.isDraft()) {
table.adjustReferences(this);
}
}
}
/**
* Return the map of all the tables.
*/
@@ -5,7 +5,6 @@ import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.dbmigration.migration.IdentityType;
import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.visitor.BeanPropertyVisitor;
import com.avaje.ebean.dbmigration.model.visitor.BeanVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
@@ -29,7 +28,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
* This creates an MTable and adds it to the model.
* </p>
*/
public BeanPropertyVisitor visitBean(BeanDescriptor<?> descriptor) {
public ModelBuildPropertyVisitor visitBean(BeanDescriptor<?> descriptor) {
if (!descriptor.isInheritanceRoot()) {
return null;
@@ -58,7 +57,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
table.addColumn(new MColumn(discColumn, discDbType, true));
}
return new ModelBuildPropertyVisitor(ctx, table, descriptor.getCompoundUniqueConstraints());
return new ModelBuildPropertyVisitor(ctx, table, descriptor);
}
/**
@@ -31,6 +31,14 @@ public class ModelBuildContext {
this.maxLength = maxLength;
}
/**
* Adjust the foreign key references on any draft tables (that reference other draft tables).
* This is called as a 'second pass' after all the draft tables have been identified.
*/
public void adjustDraftReferences() {
model.adjustDraftReferences();
}
public String primaryKeyName(String tableName) {
return maxLength(constraintNaming.primaryKeyName(tableName), 0);
}
@@ -5,6 +5,7 @@ import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MCompoundForeignKey;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.visitor.BaseTablePropertyVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
@@ -14,6 +15,7 @@ import com.avaje.ebeaninternal.server.deploy.TableJoinColumn;
import com.avaje.ebeaninternal.server.deploy.id.ImportedId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -26,6 +28,8 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
private final MTable table;
private final BeanDescriptor<?> beanDescriptor;
private final IndexSet indexSet = new IndexSet();
private MColumn lastColumn;
@@ -36,11 +40,11 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
private int countCheck;
public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, CompoundUniqueConstraint[] constraints) {
public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, BeanDescriptor<?> beanDescriptor) {
this.ctx = ctx;
this.table = table;
addCompoundUniqueConstraint(constraints);
this.beanDescriptor = beanDescriptor;
addCompoundUniqueConstraint(beanDescriptor.getCompoundUniqueConstraints());
}
/**
@@ -97,6 +101,36 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
compoundKey.setIndexName(null);
}
}
addDraftTable();
}
/**
* Create a 'draft' table that is mostly the same as the base table.
* It has @DraftOnly columns and adjusted primary and foreign keys.
*/
private void addDraftTable() {
if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) {
// create a 'Draft' table which looks very similar (change PK, FK etc)
MTable draftTable = table.createDraftTable();
draftTable.setPkName(ctx.primaryKeyName(draftTable.getName()));
int fkCount = 0;
int ixCount = 0;
Collection<MColumn> cols = draftTable.getColumns().values();
for (MColumn col: cols) {
if (col.getForeignKeyName() != null) {
// Note that we adjust the 'references' table later in a second pass
// after we know all the tables that are 'draftable'
//col.setReferences(refTable + "." + refColumn);
col.setForeignKeyName(ctx.foreignKeyConstraintName(draftTable.getName(), col.getName(), ++fkCount));
String[] indexCols = {col.getName()};
col.setForeignKeyIndex(ctx.foreignKeyIndexName(draftTable.getName(), indexCols, ++ixCount));
}
}
ctx.addTable(draftTable);
}
}
@@ -213,6 +247,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
}
MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p));
col.setDraftOnly(p.isDraftOnly());
if (p.isId()) {
col.setPrimaryKey(true);
@@ -1,5 +1,6 @@
package com.avaje.ebean.dbmigration.model.visitor;
import com.avaje.ebean.dbmigration.model.build.ModelBuildPropertyVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
/**
@@ -12,6 +13,6 @@ public interface BeanVisitor {
* Visit a BeanDescriptor and return a PropertyVisitor to use to visit each
* property on the entity bean (return null to skip visiting this bean).
*/
BeanPropertyVisitor visitBean(BeanDescriptor<?> descriptor);
ModelBuildPropertyVisitor visitBean(BeanDescriptor<?> descriptor);
}
@@ -94,6 +94,11 @@ public interface SpiQuery<T> extends Query<T> {
}
enum TemporalMode {
/**
* Query runs against draft tables.
*/
DRAFT,
/**
* Query runs against current data (normal).
*/
@@ -171,6 +176,11 @@ public interface SpiQuery<T> extends Query<T> {
*/
boolean isAsOfQuery();
/**
* Return true if this is a 'As Draft' query.
*/
boolean isAsDraft();
/**
* Return the asOf Timestamp which the query should run as.
*/
@@ -1625,6 +1625,27 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
}
public <T> void publish(Query<T> query, Transaction transaction) {
TransWrapper wrap = initTransIfRequired(transaction);
try {
SpiTransaction trans = wrap.transaction;
persister.publish(query, trans);
wrap.commitIfCreated();
} catch (RuntimeException e) {
wrap.rollbackIfCreated();
throw e;
}
}
@Override
public <T> void publish(Class<T> beanType, Object id, Transaction transaction) {
Query<T> query = find(beanType).setId(id);
publish(query, transaction);
}
private EntityBean checkEntityBean(Object bean) {
if (bean == null) {
throw new IllegalArgumentException(Message.msg("bean.isnull"));
@@ -127,13 +127,14 @@ public class InternalConfiguration {
this.beanDescriptorManager = new BeanDescriptorManager(this);
Map<String, String> asOfTableMapping = beanDescriptorManager.deploy();
Map<String, String> draftTableMap = beanDescriptorManager.getDraftTableMap();
this.transactionManager = createTransactionManager();
DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform();
this.binder = getBinder(typeManager, databasePlatform);
this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod());
this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod(), draftTableMap);
ExternalTransactionManager externalTransactionManager = serverConfig.getExternalTransactionManager();
if (externalTransactionManager == null && serverConfig.isUseJtaTransactionManager()) {
@@ -119,6 +119,8 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
*/
private boolean requestUpdateAllLoadedProps;
private boolean publish;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse) {
@@ -736,4 +738,51 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
return requestUpdateAllLoadedProps;
}
/**
* This is a persist request for a 'publish' action.
*/
public void setPublish() {
publish = true;
}
/**
* Return true if this request is a 'publish' action.
*/
public boolean isPublish() {
return publish;
}
/**
* Return the key for an update persist request.
*/
public int getUpdatePlanHash() {
int hash;
if (determineUpdateAllLoadedProperties()) {
hash = intercept.getLoadedPropertyHash();
} else {
hash = intercept.getDirtyPropertyHash();
}
BeanProperty versionProperty = beanDescriptor.getVersionProperty();
if (versionProperty != null) {
if (intercept.isLoadedProperty(versionProperty.getPropertyIndex())) {
hash = hash * 31 + 7;
}
}
if (publish) {
hash = hash * 31;
}
return hash;
}
/**
* Return the table to update depending if the request is a 'publish' one or normal.
*/
public String getUpdateTable() {
return publish ? beanDescriptor.getBaseTable() : beanDescriptor.getDraftTable();
}
}
@@ -3,12 +3,12 @@ package com.avaje.ebeaninternal.server.core;
import java.util.Collection;
import com.avaje.ebean.CallableSql;
import com.avaje.ebean.Query;
import com.avaje.ebean.SqlUpdate;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.Update;
import com.avaje.ebean.bean.EntityBean;
/**
* API for persisting a bean.
*/
@@ -17,77 +17,79 @@ public interface Persister {
/**
* Update the bean.
*/
void update(EntityBean entityBean, Transaction t);
void update(EntityBean entityBean, Transaction t);
/**
* Update the bean specifying deleteMissingChildren.
*/
void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren);
/**
* Update the bean specifying deleteMissingChildren.
*/
void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren);
/**
* Force an Insert using the given bean.
*/
void insert(EntityBean entityBean, Transaction t);
/**
* Force an Insert using the given bean.
*/
void insert(EntityBean entityBean, Transaction t);
/**
* Insert or update the bean depending on its state.
*/
void save(EntityBean entityBean, Transaction t);
/**
* Insert or update the bean depending on its state.
*/
void save(EntityBean entityBean, Transaction t);
/**
* Save the associations of a ManyToMany given the owner bean and the
* propertyName of the ManyToMany collection.
*/
void saveManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Save the associations of a ManyToMany given the owner bean and the
* propertyName of the ManyToMany collection.
*/
void saveManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Save an association (OneToMany, ManyToOne, OneToOne or ManyToMany).
*
* @param parentBean
* the bean that owns the association.
* @param propertyName
* the name of the property to save.
* @param t
* the transaction to use.
*/
void saveAssociation(EntityBean parentBean, String propertyName, Transaction t);
/**
* Save an association (OneToMany, ManyToOne, OneToOne or ManyToMany).
*
* @param parentBean the bean that owns the association.
* @param propertyName the name of the property to save.
* @param t the transaction to use.
*/
void saveAssociation(EntityBean parentBean, String propertyName, Transaction t);
/**
* Delete the associations of a ManyToMany given the owner bean and the property name of the ManyToMany.
*/
int deleteManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Delete the associations of a ManyToMany given the owner bean and the property name of the ManyToMany.
*/
int deleteManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Delete a bean given it's type and id value.
* <p>
* This will also cascade delete one level of children.
* </p>
*/
int delete(Class<?> beanType, Object id, Transaction transaction);
/**
* Delete a bean given it's type and id value.
* <p>
* This will also cascade delete one level of children.
* </p>
*/
int delete(Class<?> beanType, Object id, Transaction transaction);
/**
* Delete the bean.
*/
void delete(EntityBean entityBean, Transaction t);
/**
* Delete the bean.
*/
void delete(EntityBean entityBean, Transaction t);
/**
* Delete multiple beans given a collection of Id values.
*/
void deleteMany(Class<?> beanType, Collection<?> ids, Transaction transaction);
/**
* Delete multiple beans given a collection of Id values.
*/
void deleteMany(Class<?> beanType, Collection<?> ids, Transaction transaction);
/**
* Execute the Update.
*/
int executeOrmUpdate(Update<?> update, Transaction t);
/**
* Execute the UpdateSql.
*/
int executeSqlUpdate(SqlUpdate update, Transaction t);
/**
* Execute the CallableSql.
*/
int executeCallable(CallableSql callable, Transaction t);
/**
* Publish the draft beans matching the given query.
*/
<T> void publish(Query<T> query, Transaction transaction);
/**
* Execute the Update.
*/
int executeOrmUpdate(Update<?> update, Transaction t);
/**
* Execute the UpdateSql.
*/
int executeSqlUpdate(SqlUpdate update, Transaction t);
/**
* Execute the CallableSql.
*/
int executeCallable(CallableSql callable, Transaction t);
}
@@ -147,11 +147,17 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final String baseTableVersionsBetween;
private final boolean historySupport;
private final String draftTable;
/**
* Set to true if read auditing is on for this bean type.
*/
private final boolean readAuditing;
private final boolean draftable;
private final boolean draftableElement;
/**
* Map of BeanProperty Linked so as to preserve order.
*/
@@ -319,7 +325,8 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final boolean updateChangesOnly;
private final boolean cacheSharableBeans;
private final BeanDescriptorDraftHelp<T> draftHelp;
private final BeanDescriptorCacheHelp<T> cacheHelp;
private final BeanDescriptorJsonHelp<T> jsonHelp;
@@ -372,7 +379,10 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.compoundUniqueConstraints = deploy.getCompoundUniqueConstraints();
this.readAuditing = deploy.isReadAuditing();
this.draftable = deploy.isDraftable();
this.draftableElement = deploy.isDraftableElement();
this.historySupport = deploy.isHistorySupport();
this.draftTable = deploy.getDraftTable();
this.baseTable = InternString.intern(deploy.getBaseTable());
this.baseTableAsOf = deploy.getBaseTableAsOf();
this.baseTableVersionsBetween = deploy.getBaseTableVersionsBetween();
@@ -413,6 +423,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly();
this.cacheHelp = new BeanDescriptorCacheHelp<T>(this, owner.getCacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
this.jsonHelp = new BeanDescriptorJsonHelp<T>(this);
this.draftHelp = new BeanDescriptorDraftHelp<T>(this);
// Check if there are no cascade save associated beans ( subject to change
// in initialiseOther()). Note that if we are in an inheritance hierarchy
@@ -525,12 +536,15 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
* </p>
* @param withHistoryTables map populated if @History is supported on this entity bean
*/
public void initialiseId(Map<String, String> withHistoryTables) {
public void initialiseId(Map<String, String> withHistoryTables, Map<String,String> draftTables) {
if (logger.isTraceEnabled()) {
logger.trace("BeanDescriptor initialise " + fullName);
}
if (draftable) {
draftTables.put(baseTable, draftTable);
}
if (historySupport) {
// add mapping (used to swap out baseTable for asOf queries)
withHistoryTables.put(baseTable, baseTableAsOf);
@@ -810,6 +824,10 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return inheritInfo == null || inheritInfo.isRoot();
}
public T publish(T draftBean, T liveBean) {
return draftHelp.publish(draftBean, liveBean);
}
/**
* Set the bean caching on or off.
*/
@@ -1856,12 +1874,20 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
*/
public String getBaseTable(SpiQuery.TemporalMode mode) {
switch (mode) {
case DRAFT: return draftTable;
case VERSIONS: return baseTableVersionsBetween;
case AS_OF: return baseTableAsOf;
default: return baseTable;
}
}
/**
* Return the associated draft table.
*/
public String getDraftTable() {
return draftTable;
}
/**
* Return true if read auditing is on this entity bean.
*/
@@ -1869,6 +1895,20 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return readAuditing;
}
/**
* Return true if this entity type is draftable.
*/
public boolean isDraftable() {
return draftable;
}
/**
* Return true if this entity type is a draftable element (child).
*/
public boolean isDraftableElement() {
return draftableElement;
}
/**
* Return true if this entity bean has history support.
*/
@@ -0,0 +1,43 @@
package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.bean.EntityBean;
/**
* Helper for BeanDescriptor that manages draft entity beans.
*
* @param <T> The entity bean type
*/
public final class BeanDescriptorDraftHelp<T> {
private final BeanDescriptor<T> desc;
public BeanDescriptorDraftHelp(BeanDescriptor<T> desc) {
this.desc = desc;
}
/**
*
*/
public void initialise() {
}
public T publish(T draftBean, T liveBean) {
if (liveBean == null) {
liveBean = (T)desc.createEntityBean();
}
EntityBean draft = (EntityBean)draftBean;
EntityBean live = (EntityBean)liveBean;
BeanProperty idProperty = desc.getIdProperty();
idProperty.publish(draft, live);
BeanProperty[] props = desc.propertiesNonMany();
for (int i = 0; i < props.length; i++) {
props[i].publish(draft, live);
}
return liveBean;
}
}
@@ -167,6 +167,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
*/
private final Map<String,String> asOfTableMap = new HashMap<String, String>();
/**
* Map of base tables to 'draft' tables.
*/
private final Map<String,String> draftTableMap = new HashMap<String, String>();
/**
* Create for a given database dbConfig.
*/
@@ -269,6 +274,13 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
return idBinderFactory.createIdBinder(idProperty);
}
/**
* Return the map of base tables to draft tables.
*/
public Map<String,String> getDraftTableMap() {
return draftTableMap;
}
/**
* Deploy returning the asOfTableMap (which is required by the SQL builders).
*/
@@ -396,7 +408,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(asOfTableMap);
d.initialiseId(asOfTableMap, draftTableMap);
}
// PASS 2:
@@ -223,6 +223,10 @@ public class BeanProperty implements ElPropertyValue {
final boolean jsonDeserialize;
final boolean draftOnly;
final boolean draftDirty;
final boolean indexed;
final String indexName;
@@ -249,6 +253,8 @@ public class BeanProperty implements ElPropertyValue {
this.dbInsertable = deploy.isDbInsertable();
this.dbUpdatable = deploy.isDbUpdateable();
this.excludedFromHistory = deploy.isExcludedFromHistory();
this.draftDirty = deploy.isDraftDirty();
this.draftOnly = deploy.isDraftOnly();
this.secondaryTable = deploy.isSecondaryTable();
if (secondaryTable) {
@@ -333,6 +339,8 @@ public class BeanProperty implements ElPropertyValue {
this.formula = false;
this.excludedFromHistory = source.excludedFromHistory;
this.draftDirty = source.draftDirty;
this.draftOnly = source.draftOnly;
this.fetchEager = source.fetchEager;
this.unidirectionalShadow = source.unidirectionalShadow;
this.discriminator = source.discriminator;
@@ -493,7 +501,7 @@ public class BeanProperty implements ElPropertyValue {
if (formula) {
ctx.appendFormulaSelect(sqlFormulaSelect);
} else if (!isTransient) {
} else if (!isTransient && !ignoreDraftOnlyProperty(ctx.isDraftQuery())) {
if (secondaryTableJoin != null) {
String relativePrefix = ctx.getRelativePrefix(secondaryTableJoinPrefix);
@@ -592,6 +600,18 @@ public class BeanProperty implements ElPropertyValue {
return local;
}
/**
* Copy/set the property value from the draft bean to the live bean.
*/
public void publish(EntityBean draftBean, EntityBean liveBean) {
if (!version && !draftOnly) {
// set property value from draft to live
Object value = getValueIntercept(draftBean);
setValueIntercept(liveBean, value);
}
}
/**
* Set the value of the property without interception or
* PropertyChangeSupport.
@@ -600,10 +620,7 @@ public class BeanProperty implements ElPropertyValue {
try {
setter.set(bean, value);
} catch (Exception ex) {
String beanType = bean == null ? "null" : bean.getClass().getName();
String msg = "set " + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType
+ "] threw error";
throw new RuntimeException(msg, ex);
throw new RuntimeException(setterErrorMsg(bean, value, "set "), ex);
}
}
@@ -614,13 +631,18 @@ public class BeanProperty implements ElPropertyValue {
try {
setter.setIntercept(bean, value);
} catch (Exception ex) {
String beanType = bean == null ? "null" : bean.getClass().getName();
String msg = "setIntercept " + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType
+ "] threw error";
throw new RuntimeException(msg, ex);
throw new RuntimeException(setterErrorMsg(bean, value, "setIntercept "), ex);
}
}
/**
* Return an error message when calling a setter.
*/
private String setterErrorMsg(EntityBean bean, Object value, String prefix) {
String beanType = bean == null ? "null" : bean.getClass().getName();
return prefix + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType + "] threw error";
}
public Object getCacheDataValue(EntityBean bean) {
return getValue(bean);
}
@@ -900,8 +922,16 @@ public class BeanProperty implements ElPropertyValue {
/**
* Return true if this property is loadable from a resultSet.
*/
public boolean isLoadProperty() {
return !isTransient || formula;
public boolean isLoadProperty(boolean draftQuery) {
return !ignoreDraftOnlyProperty(draftQuery) && (!isTransient || formula);
}
/**
* Return true if this is a draftOnly property on a non-asDraft query and as such this
* property should not be included in a sql query.
*/
protected boolean ignoreDraftOnlyProperty(boolean draftQuery) {
return draftOnly && !draftQuery;
}
/**
@@ -951,7 +981,7 @@ public class BeanProperty implements ElPropertyValue {
return lob;
}
private boolean isLobType(int type) {
public static boolean isLobType(int type) {
switch (type) {
case Types.CLOB:
return true;
@@ -1000,6 +1030,21 @@ public class BeanProperty implements ElPropertyValue {
return excludedFromHistory;
}
/**
* Return true if this property only exists on the draft table.
*/
public boolean isDraftOnly() {
return draftOnly;
}
/**
* Return true if this property is a boolean flag only on the draft table
* indicating that when the draft is different from the published row.
*/
public boolean isDraftDirty() {
return draftDirty;
}
/**
* Return true if this property should be included in an Insert.
*/
@@ -79,4 +79,9 @@ public interface DbReadContext {
* Return the query mode.
*/
SpiQuery.Mode getQueryMode();
/**
* Return true if the underlying query is a 'asDraft' query.
*/
boolean isDraftQuery();
}
@@ -120,4 +120,9 @@ public interface DbSqlContext {
*/
void appendHistorySysPeriod();
/**
* Return true if the query is a 'asDraft' query.
*/
boolean isDraftQuery();
}
@@ -118,10 +118,16 @@ public class DeployBeanDescriptor<T> {
private String baseTableVersionsBetween;
private String draftTable;
private boolean historySupport;
private boolean readAuditing;
private boolean draftable;
private boolean draftableElement;
private TableName baseTableFull;
private String[] properties;
@@ -201,6 +207,22 @@ public class DeployBeanDescriptor<T> {
return readAuditing;
}
public void setDraftable() {
draftable = true;
}
public boolean isDraftable() {
return draftable;
}
public void setDraftableElement() {
draftableElement = true;
}
public boolean isDraftableElement() {
return draftableElement;
}
public boolean isScalaObject() {
Class<?>[] interfaces = beanType.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
@@ -485,6 +507,10 @@ public class DeployBeanDescriptor<T> {
postLoaders.add(postLoad);
}
public String getDraftTable() {
return draftTable;
}
/**
* Return the base table. Only properties mapped to the base table are by
* default persisted.
@@ -522,6 +548,7 @@ public class DeployBeanDescriptor<T> {
this.baseTable = baseTableFull == null ? null : baseTableFull.getQualifiedName();
this.baseTableAsOf = baseTable + asOfSuffix;
this.baseTableVersionsBetween = baseTable + versionsBetweenSuffix;
this.draftTable = (draftable) ? baseTable+"_draft" : baseTable;
}
public void sortProperties() {
@@ -8,6 +8,7 @@ import com.avaje.ebean.config.ScalarTypeConverter;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.config.dbplatform.DbEncryptFunction;
import com.avaje.ebeaninternal.server.core.InternString;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import com.avaje.ebeaninternal.server.properties.BeanPropertyGetter;
@@ -182,6 +183,9 @@ public class DeployBeanProperty {
private boolean excludedFromHistory;
private boolean draftOnly;
private boolean draftDirty;
public DeployBeanProperty(DeployBeanDescriptor<?> desc, Class<?> propertyType, ScalarType<?> scalarType, ScalarTypeConverter<?, ?> typeConverter) {
this.desc = desc;
this.propertyType = propertyType;
@@ -598,7 +602,7 @@ public class DeployBeanProperty {
*/
public void setDbType(int dbType) {
this.dbType = dbType;
this.lob = isLobType(dbType);
this.lob = BeanProperty.isLobType(dbType);
}
/**
@@ -609,22 +613,6 @@ public class DeployBeanProperty {
return lob;
}
private boolean isLobType(int type) {
switch (type) {
case Types.CLOB:
return true;
case Types.BLOB:
return true;
case Types.LONGVARBINARY:
return true;
case Types.LONGVARCHAR:
return true;
default:
return false;
}
}
public boolean isDbNumberType() {
return isNumericType(dbType);
}
@@ -849,4 +837,21 @@ public class DeployBeanProperty {
public void setExcludedFromHistory() {
this.excludedFromHistory = true;
}
public void setDraftOnly() {
this.draftOnly = true;
}
public boolean isDraftOnly() {
return draftOnly;
}
public void setDraftDirty() {
this.draftOnly = true;
this.draftDirty = true;
}
public boolean isDraftDirty() {
return draftDirty;
}
}
@@ -9,6 +9,8 @@ import javax.persistence.UniqueConstraint;
import com.avaje.ebean.annotation.CacheStrategy;
import com.avaje.ebean.annotation.CacheTuning;
import com.avaje.ebean.annotation.Draftable;
import com.avaje.ebean.annotation.DraftableElement;
import com.avaje.ebean.annotation.EntityConcurrencyMode;
import com.avaje.ebean.annotation.History;
import com.avaje.ebean.annotation.Index;
@@ -98,6 +100,16 @@ public class AnnotationClass extends AnnotationParser {
}
}
Draftable draftable = cls.getAnnotation(Draftable.class);
if (draftable != null) {
descriptor.setDraftable();
}
DraftableElement draftableElement = cls.getAnnotation(DraftableElement.class);
if (draftableElement != null) {
descriptor.setDraftableElement();
}
ReadAudit readAudit = cls.getAnnotation(ReadAudit.class);
if (readAudit != null) {
descriptor.setReadAuditing();
@@ -149,6 +149,14 @@ public class AnnotationFields extends AnnotationParser {
util.setLobType(prop);
}
if (get(prop, DraftOnly.class) != null) {
prop.setDraftOnly();
}
if (get(prop, DraftDirty.class) != null) {
prop.setDraftDirty();
}
DbJson dbJson = get(prop, DbJson.class);
if (dbJson != null) {
util.setDbJsonType(prop, dbJson);
@@ -83,17 +83,7 @@ public final class DefaultPersister implements Persister {
*/
public int executeCallable(CallableSql callSql, Transaction t) {
PersistRequestCallableSql request = new PersistRequestCallableSql(server, callSql, (SpiTransaction) t, persistExecute);
try {
request.initTransIfRequired();
int rc = request.executeOrQueue();
request.commitTransIfRequired();
return rc;
} catch (RuntimeException e) {
request.rollbackTransIfRequired();
throw e;
}
return executeOrQueue(new PersistRequestCallableSql(server, callSql, (SpiTransaction) t, persistExecute));
}
/**
@@ -110,7 +100,10 @@ public final class DefaultPersister implements Persister {
throw new PersistenceException(msg);
}
PersistRequestOrmUpdate request = new PersistRequestOrmUpdate(server, mgr, ormUpdate, (SpiTransaction) t, persistExecute);
return executeOrQueue(new PersistRequestOrmUpdate(server, mgr, ormUpdate, (SpiTransaction) t, persistExecute));
}
private int executeOrQueue(PersistRequest request) {
try {
request.initTransIfRequired();
int rc = request.executeOrQueue();
@@ -128,17 +121,52 @@ public final class DefaultPersister implements Persister {
*/
public int executeSqlUpdate(SqlUpdate updSql, Transaction t) {
PersistRequestUpdateSql request = new PersistRequestUpdateSql(server, updSql, (SpiTransaction) t, persistExecute);
try {
request.initTransIfRequired();
int rc = request.executeOrQueue();
request.commitTransIfRequired();
return rc;
return executeOrQueue(new PersistRequestUpdateSql(server, updSql, (SpiTransaction) t, persistExecute));
}
} catch (RuntimeException e) {
request.rollbackTransIfRequired();
throw e;
@Override
public <T> void publish(Query<T> query, Transaction transaction) {
query.asDraft();
Class<T> beanType = query.getBeanType();
List<T> draftBeans = server.findList(query, transaction);
BeanDescriptor<T> desc = server.getBeanDescriptor(beanType);
// get the list of Id's
List<Object> idList = new ArrayList<Object>();
for (T draftBean: draftBeans) {
idList.add(desc.getBeanId(draftBean));
}
// fetch existing live beans to update (or insert if missing)
Map<?, T> liveBeans = server.find(beanType)
.where().idIn(idList)
.findMap();
List<T> livePublish = new ArrayList<T>(idList.size());
BeanManager<T> mgr = beanDescriptorManager.getBeanManager(beanType);
for (T draftBean: draftBeans) {
Object draftID = desc.getBeanId(draftBean);
T existingLiveBean = liveBeans.get(draftID);
T liveBean = desc.publish(draftBean, existingLiveBean);
livePublish.add(liveBean);
Type persistType = (existingLiveBean == null) ? Type.INSERT : Type.UPDATE;
PersistRequestBean<T> request = createRequest(liveBean, transaction, null, mgr, persistType, false);
request.setPublish();
if (persistType == Type.INSERT) {
insert(request);
} else {
update(request);
}
}
}
/**
@@ -55,7 +55,7 @@ public class InsertHandler extends DmlHandler {
public InsertHandler(PersistRequestBean<?> persist, InsertMeta meta) {
super(persist, meta.isEmptyStringToNull());
this.meta = meta;
this.concatinatedKey = meta.isConcatinatedKey();
this.concatinatedKey = meta.isConcatenatedKey();
}
/**
@@ -90,7 +90,7 @@ public class InsertHandler extends DmlHandler {
SpiTransaction t = persistRequest.getTransaction();
// get the appropriate sql
sql = meta.getSql(withId);
sql = meta.getSql(withId, persistRequest.isPublish());
PreparedStatement pstmt;
if (persistRequest.isBatched()) {
@@ -101,7 +101,7 @@ public class InsertHandler extends DmlHandler {
dataBind = new DataBind(pstmt);
// bind the bean property values
meta.bind(this, bean, withId);
meta.bind(this, bean, withId, persistRequest.isPublish());
logSql(sql);
}
@@ -167,15 +167,7 @@ public class InsertHandler extends DmlHandler {
ResultSet rset = dataBind.getPstmt().getGeneratedKeys();
try {
if (rset.next()) {
Object idValue = rset.getObject(1);
if (idValue != null) {
persistRequest.setGeneratedKey(idValue);
}
} else {
throw new PersistenceException(Message.msg("persist.autoinc.norows"));
}
setGeneratedKey(rset);
} finally {
try {
rset.close();
@@ -186,6 +178,18 @@ public class InsertHandler extends DmlHandler {
}
}
private void setGeneratedKey(ResultSet rset) throws SQLException {
if (rset.next()) {
Object idValue = rset.getObject(1);
if (idValue != null) {
persistRequest.setGeneratedKey(idValue);
}
} else {
throw new PersistenceException(Message.msg("persist.autoinc.norows"));
}
}
/**
* For non batch insert with DBs that do not support getGeneratedKeys. Use a
* SQL select to fetch back the Id value.
@@ -199,14 +203,7 @@ public class InsertHandler extends DmlHandler {
try {
stmt = conn.prepareStatement(selectLastInsertedId);
rset = stmt.executeQuery();
if (rset.next()) {
Object idValue = rset.getObject(1);
if (idValue != null) {
persistRequest.setGeneratedKey(idValue);
}
} else {
throw new PersistenceException(Message.msg("persist.autoinc.norows"));
}
setGeneratedKey(rset);
} finally {
try {
if (rset != null) {
@@ -8,6 +8,7 @@ import com.avaje.ebeaninternal.server.deploy.InheritInfo;
import com.avaje.ebeaninternal.server.persist.dmlbind.Bindable;
import com.avaje.ebeaninternal.server.persist.dmlbind.BindableDiscriminator;
import com.avaje.ebeaninternal.server.persist.dmlbind.BindableId;
import com.avaje.ebeaninternal.server.persist.dmlbind.BindableList;
import java.sql.SQLException;
@@ -18,21 +19,22 @@ import java.sql.SQLException;
public final class InsertMeta {
private final String sqlNullId;
private final String sqlWithId;
private final String sqlDraftNullId;
private final String sqlDraftWithId;
private final BindableId id;
private final Bindable discriminator;
private final Bindable all;
private final BindableList all;
private final BindableList allExcludeDraftOnly;
private final boolean supportsGetGeneratedKeys;
private final boolean concatinatedKey;
private final String tableName;
/**
* Used for DB that do not support getGeneratedKeys.
*/
@@ -44,16 +46,20 @@ public final class InsertMeta {
private final boolean emptyStringToNull;
public InsertMeta(DatabasePlatform dbPlatform, BeanDescriptor<?> desc, Bindable shadowFKey, BindableId id, Bindable all) {
public InsertMeta(DatabasePlatform dbPlatform, BeanDescriptor<?> desc, Bindable shadowFKey, BindableId id, BindableList all) {
this.emptyStringToNull = dbPlatform.isTreatEmptyStringsAsNull();
this.tableName = desc.getBaseTable();
this.discriminator = getDiscriminator(desc);
this.id = id;
this.all = all;
this.allExcludeDraftOnly = all.excludeDraftOnly();
this.shadowFKey = shadowFKey;
this.sqlWithId = genSql(false);
String tableName = desc.getBaseTable();
String draftTableName = desc.getDraftTable();
this.sqlWithId = genSql(false, tableName, false);
this.sqlDraftWithId = desc.isDraftable() ? genSql(false, draftTableName, true) : sqlWithId;
// only available for single Id property
if (id.isConcatenated()) {
@@ -61,6 +67,7 @@ public final class InsertMeta {
this.concatinatedKey = true;
this.identityDbColumns = null;
this.sqlNullId = null;
this.sqlDraftNullId = null;
this.supportsGetGeneratedKeys = false;
this.selectLastInsertedId = null;
@@ -76,7 +83,8 @@ public final class InsertMeta {
this.supportsGetGeneratedKeys = dbPlatform.getDbIdentity().isSupportsGetGeneratedKeys();
this.selectLastInsertedId = desc.getSelectLastInsertedId();
}
this.sqlNullId = genSql(true);
this.sqlNullId = genSql(true, tableName, false);
this.sqlDraftNullId = desc.isDraftable() ? genSql(false, draftTableName, true) : sqlNullId;
}
}
@@ -99,7 +107,7 @@ public final class InsertMeta {
/**
* Return true if this is a concatenated key.
*/
public boolean isConcatinatedKey() {
public boolean isConcatenatedKey() {
return concatinatedKey;
}
@@ -137,7 +145,7 @@ public final class InsertMeta {
/**
* Bind the request based on whether the id value(s) are null.
*/
public void bind(DmlHandler request, EntityBean bean, boolean withId) throws SQLException {
public void bind(DmlHandler request, EntityBean bean, boolean withId, boolean publish) throws SQLException {
if (withId) {
id.dmlBind(request, bean);
@@ -148,27 +156,31 @@ public final class InsertMeta {
if (discriminator != null) {
discriminator.dmlBind(request, bean);
}
all.dmlBind(request, bean);
if (publish) {
allExcludeDraftOnly.dmlBind(request, bean);
} else {
all.dmlBind(request, bean);
}
}
/**
* get the sql based whether the id value(s) are null.
*/
public String getSql(boolean withId) {
public String getSql(boolean withId, boolean publish) {
if (withId) {
return sqlWithId;
return publish ? sqlWithId : sqlDraftWithId;
} else {
return sqlNullId;
return publish ? sqlNullId : sqlDraftNullId;
}
}
private String genSql(boolean nullId) {
private String genSql(boolean nullId, String table, boolean draftTable) {
GenerateDmlRequest request = new GenerateDmlRequest();
request.setInsertSetMode();
request.append("insert into ").append(tableName);
request.append("insert into ").append(table);
request.append(" (");
if (!nullId) {
@@ -183,7 +195,11 @@ public final class InsertMeta {
discriminator.dmlAppend(request);
}
all.dmlAppend(request);
if (draftTable) {
all.dmlAppend(request);
} else {
allExcludeDraftOnly.dmlAppend(request);
}
request.append(") values (");
request.append(request.getInsertBindBuffer());
@@ -96,7 +96,7 @@ public class MetaFactory {
embeddedFact.create(allList, desc, DmlMode.INSERT, includeLobs);
assocOneFact.create(allList, desc, DmlMode.INSERT);
Bindable allBindable = new BindableList(allList);
BindableList allBindable = new BindableList(allList);
BeanPropertyAssocOne<?> unidirectional = desc.getUnidirectional();
@@ -2,11 +2,9 @@ package com.avaje.ebeaninternal.server.persist.dml;
import com.avaje.ebean.annotation.ConcurrencyMode;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.EntityBeanIntercept;
import com.avaje.ebeaninternal.api.SpiUpdatePlan;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.persist.dmlbind.Bindable;
import com.avaje.ebeaninternal.server.persist.dmlbind.BindableId;
import com.avaje.ebeaninternal.server.persist.dmlbind.BindableList;
@@ -39,8 +37,8 @@ public final class UpdateMeta {
this.id = id;
this.version = version;
String sqlNone = genSql(ConcurrencyMode.NONE, set);
String sqlVersion = genSql(ConcurrencyMode.VERSION, set);
String sqlNone = genSql(ConcurrencyMode.NONE, set, desc.getBaseTable());
String sqlVersion = genSql(ConcurrencyMode.VERSION, set, desc.getBaseTable());
this.modeNoneUpdatePlan = new UpdatePlan(ConcurrencyMode.NONE, sqlNone, set);
this.modeVersionUpdatePlan = new UpdatePlan(ConcurrencyMode.VERSION, sqlVersion, set);
@@ -106,27 +104,10 @@ public final class UpdateMeta {
private SpiUpdatePlan getDynamicUpdatePlan(PersistRequestBean<?> persistRequest) {
EntityBeanIntercept ebi = persistRequest.getEntityBeanIntercept();
int hash;
if (persistRequest.determineUpdateAllLoadedProperties()) {
hash = ebi.getLoadedPropertyHash();
} else {
hash = ebi.getDirtyPropertyHash();
}
BeanDescriptor<?> beanDescriptor = persistRequest.getBeanDescriptor();
BeanProperty versionProperty = beanDescriptor.getVersionProperty();
if (versionProperty != null) {
if (ebi.isLoadedProperty(versionProperty.getPropertyIndex())) {
hash = hash * 31 + 7;
}
}
Integer key = hash;
int key = persistRequest.getUpdatePlanHash();
// check if we can use a cached UpdatePlan
BeanDescriptor<?> beanDescriptor = persistRequest.getBeanDescriptor();
SpiUpdatePlan updatePlan = beanDescriptor.getUpdatePlan(key);
if (updatePlan != null) {
return updatePlan;
@@ -142,7 +123,7 @@ public final class UpdateMeta {
ConcurrencyMode mode = persistRequest.determineConcurrencyMode();
// build the SQL for this update statement
String sql = genSql(mode, bindableList);
String sql = genSql(mode, bindableList, persistRequest.getUpdateTable());
updatePlan = new UpdatePlan(key, mode, sql, bindableList);
@@ -152,7 +133,7 @@ public final class UpdateMeta {
return updatePlan;
}
private String genSql(ConcurrencyMode conMode, BindableList bindableList) {
private String genSql(ConcurrencyMode conMode, BindableList bindableList, String tableName) {
// update set col0=?, col1=?, col2=? where bcol=? and bc1=? and bc2=?
@@ -39,4 +39,8 @@ public interface Bindable {
*/
void dmlBind(BindableRequest request, EntityBean bean) throws SQLException;
/**
* Return true if the underlying property is 'draft only'.
*/
boolean isDraftOnly();
}
@@ -28,6 +28,11 @@ public class BindableAssocOne implements Bindable {
return "BindableAssocOne " + assocOne;
}
@Override
public boolean isDraftOnly() {
return assocOne.isDraftOnly();
}
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
if (request.isAddToUpdate(assocOne)) {
list.add(this);
@@ -27,6 +27,11 @@ public class BindableCompound implements Bindable {
return "BindableCompound " + compound + " items:" + Arrays.toString(items);
}
@Override
public boolean isDraftOnly() {
return false;
}
public void dmlAppend(GenerateDmlRequest request) {
for (int i = 0; i < items.length; i++) {
@@ -29,6 +29,11 @@ public class BindableDiscriminator implements Bindable {
return columnName + " = " + discValue;
}
@Override
public boolean isDraftOnly() {
return false;
}
@Override
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
throw new PersistenceException("Never called (only for inserts)");
@@ -27,6 +27,11 @@ public class BindableEmbedded implements Bindable {
return "BindableEmbedded " + embProp + " items:" + Arrays.toString(items);
}
@Override
public boolean isDraftOnly() {
return embProp.isDraftOnly();
}
public void dmlAppend(GenerateDmlRequest request) {
for (int i = 0; i < items.length; i++) {
@@ -27,6 +27,11 @@ public class BindableEncryptedProperty implements Bindable {
return prop.toString();
}
@Override
public boolean isDraftOnly() {
return prop.isDraftOnly();
}
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
if (request.isAddToUpdate(prop)) {
list.add(this);
@@ -30,6 +30,11 @@ public final class BindableIdEmbedded implements BindableId {
matches = MatchedImportedProperty.build(props, desc);
}
@Override
public boolean isDraftOnly() {
return false;
}
public boolean isEmpty() {
return false;
}
@@ -13,6 +13,11 @@ public class BindableIdEmpty implements BindableId {
return true;
}
@Override
public boolean isDraftOnly() {
return false;
}
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
// nothing
}
@@ -38,6 +38,11 @@ public final class BindableIdScalar implements BindableId {
return uidProp.toString();
}
@Override
public boolean isDraftOnly() {
return false;
}
/**
* Does nothing for BindableId.
*/
@@ -1,6 +1,7 @@
package com.avaje.ebeaninternal.server.persist.dmlbind;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.avaje.ebean.bean.EntityBean;
@@ -18,6 +19,24 @@ public class BindableList implements Bindable {
items = list.toArray(new Bindable[list.size()]);
}
/**
* Return a bindable list that excludes @DraftOnly properties.
*/
public BindableList excludeDraftOnly() {
List<Bindable> copy = new ArrayList<Bindable>(items.length);
for (Bindable b : items) {
if (!b.isDraftOnly()) {
copy.add(b);
}
}
return new BindableList(copy);
}
@Override
public boolean isDraftOnly() {
return false;
}
public void addAll(List<Bindable> list) {
for (int i = 0; i < items.length; i++) {
list.add(items[i]);
@@ -23,6 +23,11 @@ public class BindableProperty implements Bindable {
return prop.toString();
}
@Override
public boolean isDraftOnly() {
return prop.isDraftOnly();
}
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
if (request.isAddToUpdate(prop)) {
list.add(this);
@@ -38,6 +38,11 @@ public class BindableUnidirectional implements Bindable {
return "BindableShadowFKey " + unidirectional;
}
@Override
public boolean isDraftOnly() {
return false;
}
public void addToUpdate(PersistRequestBean<?> request, List<Bindable> list) {
throw new PersistenceException("Never called (for insert only)");
}
@@ -234,6 +234,11 @@ public class CQuery<T> implements DbReadContext, CancelableQuery {
}
}
@Override
public boolean isDraftQuery() {
return query.isAsDraft();
}
public Boolean isReadOnly() {
return readOnly;
}
@@ -47,15 +47,18 @@ public class CQueryBuilder {
private final CQueryHistorySupport historySupport;
private final CQueryDraftSupport draftSupport;
private final DatabasePlatform dbPlatform;
/**
* Create the SqlGenSelect.
*/
public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, CQueryHistorySupport historySupport) {
public CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
this.dbPlatform = dbPlatform;
this.binder = binder;
this.draftSupport = draftSupport;
this.historySupport = historySupport;
this.tableAliasPlaceHolder = dbPlatform.getTableAliasPlaceHolder();
this.columnAliasPrefix = dbPlatform.getColumnAliasPrefix();
@@ -103,7 +106,7 @@ public class CQueryBuilder {
predicates.prepare(true);
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query));
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query));
boolean includeJoins = sqlTree.isIncludeJoins();
@@ -146,7 +149,7 @@ public class CQueryBuilder {
// use RawSql or generated Sql
predicates.prepare(true);
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query));
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query));
SqlLimitResponse s = buildSql(null, request, predicates, sqlTree);
String sql = s.getSql();
@@ -164,6 +167,13 @@ public class CQueryBuilder {
return query.getTemporalMode() != SpiQuery.TemporalMode.CURRENT ? historySupport : null;
}
/**
* Return the draft support (or null) for a 'asDraft' query.
*/
private <T> CQueryDraftSupport getDraftSupport(SpiQuery<T> query) {
return query.getTemporalMode() == SpiQuery.TemporalMode.DRAFT ? draftSupport : null;
}
/**
* Build the row count query.
*/
@@ -205,7 +215,7 @@ public class CQueryBuilder {
predicates.prepare(true);
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query));
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query));
SqlLimitResponse s = buildSql(sqlSelect, request, predicates, sqlTree);
String sql = s.getSql();
if (hasMany || query.isRawSql()) {
@@ -255,7 +265,7 @@ public class CQueryBuilder {
// Build the tree structure that represents the query.
SpiQuery<T> query = request.getQuery();
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query));
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query));
if (query.isAsOfQuery()) {
sqlTree.addAsOfTableAlias(query);
}
@@ -294,13 +304,13 @@ public class CQueryBuilder {
* order by clauses that are not already included for the select clause.
* </p>
*/
private SqlTree createSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates, CQueryHistorySupport historySupport) {
private SqlTree createSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
if (request.isRawSql()) {
return createRawSqlSqlTree(request, predicates);
}
return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, historySupport).build();
return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, historySupport, draftSupport).build();
}
private SqlTree createRawSqlSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates) {
@@ -0,0 +1,28 @@
package com.avaje.ebeaninternal.server.query;
import java.util.Map;
/**
* Support 'asDraft' queries.
*/
public class CQueryDraftSupport {
/**
* The mapping of base tables to their matching 'draft' table.
*/
private final Map<String, String> tableMap;
public CQueryDraftSupport(Map<String, String> tableMap) {
this.tableMap = tableMap;
}
/**
* Return the draft table associated to the base table.
*
* This returns null for entities that are not draftable and in that case
* the usual base table is used.
*/
public String getDraftTable(String table) {
return tableMap.get(table);
}
}
@@ -41,10 +41,10 @@ public class CQueryEngine {
private final CQueryHistorySupport historySupport;
public CQueryEngine(DatabasePlatform dbPlatform, Binder binder, Map<String, String> asOfTableMapping, String asOfSysPeriod) {
public CQueryEngine(DatabasePlatform dbPlatform, Binder binder, Map<String, String> asOfTableMapping, String asOfSysPeriod, Map<String, String> draftTableMap) {
this.forwardOnlyHintOnFindIterate = dbPlatform.isForwardOnlyHintOnFindIterate();
this.historySupport = new CQueryHistorySupport(dbPlatform.getHistorySupport(), asOfTableMapping, asOfSysPeriod);
this.queryBuilder = new CQueryBuilder(dbPlatform, binder, historySupport);
this.queryBuilder = new CQueryBuilder(dbPlatform, binder, historySupport, new CQueryDraftSupport(draftTableMap));
}
public <T> CQuery<T> buildQuery(OrmQueryRequest<T> request) {
@@ -266,6 +266,10 @@ public class CQueryFetchIds {
// no-op
}
@Override
public boolean isDraftQuery() {
return false;
}
}
}
@@ -45,6 +45,8 @@ public class DefaultDbSqlContext implements DbSqlContext {
private ArrayList<BeanProperty> encryptedProps;
private final CQueryDraftSupport draftSupport;
private final CQueryHistorySupport historySupport;
private final boolean historyQuery;
@@ -53,12 +55,13 @@ public class DefaultDbSqlContext implements DbSqlContext {
* Construct for SELECT clause (with column alias settings).
*/
public DefaultDbSqlContext(SqlTreeAlias alias, String tableAliasPlaceHolder,
String columnAliasPrefix, boolean alwaysUseColumnAlias, CQueryHistorySupport historySupport) {
String columnAliasPrefix, boolean alwaysUseColumnAlias, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
this.alias = alias;
this.tableAliasPlaceHolder = tableAliasPlaceHolder;
this.columnAliasPrefix = columnAliasPrefix;
this.useColumnAlias = alwaysUseColumnAlias;
this.draftSupport = draftSupport;
this.historySupport = historySupport;
this.historyQuery = (historySupport != null);
}
@@ -105,22 +108,18 @@ public class DefaultDbSqlContext implements DbSqlContext {
sb.append(" ");
sb.append(type);
if (!historyQuery) {
if (draftSupport != null) {
appendTable(table, draftSupport.getDraftTable(table));
} else 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 = historySupport.getAsOfView(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(" ");
}
appendTable(table, historySupport.getAsOfView(table));
}
sb.append(a2);
sb.append(" on ");
@@ -149,6 +148,21 @@ public class DefaultDbSqlContext implements DbSqlContext {
sb.append(" ");
}
private void appendTable(String table, String draftTable) {
if (draftTable != null) {
// there is an associated history table and view so use that
sb.append(" ").append(draftTable).append(" ");
} else {
sb.append(" ").append(table).append(" ");
}
}
@Override
public boolean isDraftQuery() {
return draftSupport != null;
}
public String getTableAlias(String prefix) {
return alias.getTableAlias(prefix);
}
@@ -53,7 +53,7 @@ public class SqlBeanLoad {
public Object load(BeanProperty prop) {
if (!rawSql && !prop.isLoadProperty()) {
if (!rawSql && !prop.isLoadProperty(ctx.isDraftQuery())) {
return null;
}
@@ -98,7 +98,7 @@ public class SqlTreeBuilder {
* to the root node.
*/
public SqlTreeBuilder(String tableAliasPlaceHolder, String columnAliasPrefix,
OrmQueryRequest<?> request, CQueryPredicates predicates, CQueryHistorySupport historySupport) {
OrmQueryRequest<?> request, CQueryPredicates predicates, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
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, historySupport);
this.ctx = new DefaultDbSqlContext(alias, tableAliasPlaceHolder, columnAliasPrefix, !subQuery, historySupport, draftSupport);
}
/**
@@ -310,6 +310,11 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
return this;
}
public DefaultOrmQuery<T> asDraft() {
this.temporalMode = TemporalMode.DRAFT;
return this;
}
/**
* Set the BeanDescriptor for the root type of this query.
*/
@@ -647,6 +652,11 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
return asOf != null;
}
@Override
public boolean isAsDraft() {
return TemporalMode.DRAFT == temporalMode;
}
public void setMode(Mode mode) {
this.mode = mode;
}
@@ -732,8 +742,7 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
builder.add(mapKey);
builder.add(disableLazyLoading);
builder.add(id != null);
builder.add(asOf != null);
builder.add(versionsStart != null);
builder.add(temporalMode);
builder.add(rawSql == null ? 0 : rawSql.queryHash());
builder.add(includeTableJoin != null ? includeTableJoin.queryHash() : 0);
builder.add(rootTableAlias);