diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index 327d89275..15de32146 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -1819,4 +1819,29 @@ public interface EbeanServer { */ JsonContext json(); + /** + * Publish a single bean given its type and id. + *

+ * The values are published from the draft to the live bean. + *

+ * + * @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 the type of the entity bean + */ + void publish(Class beanType, Object id, Transaction transaction); + + /** + * Publish the beans that match the query. + *

+ * The values are published from the draft beans to the live beans. + *

+ * + * @param query the query used to select the draft beans to publish + * @param transaction the transaction the publish process should use + * @param the type of the entity bean + */ + void publish(Query query, Transaction transaction); + } diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index 4fcf88a37..fee79aafa 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -297,6 +297,11 @@ public interface Query extends Serializable { */ Query asOf(Timestamp asOf); + /** + * Execute the query against the draft set of tables. + */ + Query asDraft(); + /** * Cancel the query execution if supported by the underlying database and * driver. diff --git a/src/main/java/com/avaje/ebean/annotation/DraftDirty.java b/src/main/java/com/avaje/ebean/annotation/DraftDirty.java new file mode 100644 index 000000000..5cb9e6b65 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DraftDirty.java @@ -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 { + +} diff --git a/src/main/java/com/avaje/ebean/annotation/DraftOnly.java b/src/main/java/com/avaje/ebean/annotation/DraftOnly.java new file mode 100644 index 000000000..57d793e0f --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DraftOnly.java @@ -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 { + +} diff --git a/src/main/java/com/avaje/ebean/annotation/Draftable.java b/src/main/java/com/avaje/ebean/annotation/Draftable.java new file mode 100644 index 000000000..5110596b8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Draftable.java @@ -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 { + +} diff --git a/src/main/java/com/avaje/ebean/annotation/DraftableElement.java b/src/main/java/com/avaje/ebean/annotation/DraftableElement.java new file mode 100644 index 000000000..36dce8986 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DraftableElement.java @@ -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 { + +} diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/CurrentModel.java b/src/main/java/com/avaje/ebean/dbmigration/model/CurrentModel.java index 48bca0e0f..c0016cc7f 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/CurrentModel.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/CurrentModel.java @@ -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; } diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/MColumn.java b/src/main/java/com/avaje/ebean/dbmigration/model/MColumn.java index 80f428470..25ee8d8fc 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/MColumn.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/MColumn.java @@ -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(); diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/MTable.java b/src/main/java/com/avaje/ebean/dbmigration/model/MTable.java index e85937955..9e901bbd8 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/MTable.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/MTable.java @@ -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 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); + } + } diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/ModelContainer.java b/src/main/java/com/avaje/ebean/dbmigration/model/ModelContainer.java index 1a878215f..071ab5fa1 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/ModelContainer.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/ModelContainer.java @@ -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 tables = this.tables.values(); + for (MTable table : tables) { + if (table.isDraft()) { + table.adjustReferences(this); + } + } + } + /** * Return the map of all the tables. */ diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildBeanVisitor.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildBeanVisitor.java index 9ba81fa96..155c09de8 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildBeanVisitor.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildBeanVisitor.java @@ -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. *

*/ - 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); } /** diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java index 0cd86310e..2ec9e9b70 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java @@ -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); } diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java index abc67be4a..4f75c4d59 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java @@ -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 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); diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/visitor/BeanVisitor.java b/src/main/java/com/avaje/ebean/dbmigration/model/visitor/BeanVisitor.java index d38889317..3483c88d0 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/visitor/BeanVisitor.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/visitor/BeanVisitor.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 34d3a9e8c..565c0a800 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -94,6 +94,11 @@ public interface SpiQuery extends Query { } enum TemporalMode { + /** + * Query runs against draft tables. + */ + DRAFT, + /** * Query runs against current data (normal). */ @@ -171,6 +176,11 @@ public interface SpiQuery extends Query { */ boolean isAsOfQuery(); + /** + * Return true if this is a 'As Draft' query. + */ + boolean isAsDraft(); + /** * Return the asOf Timestamp which the query should run as. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 6ec3524ca..3d56e8e32 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1625,6 +1625,27 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } + public void publish(Query 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 void publish(Class beanType, Object id, Transaction transaction) { + + Query query = find(beanType).setId(id); + publish(query, transaction); + } + private EntityBean checkEntityBean(Object bean) { if (bean == null) { throw new IllegalArgumentException(Message.msg("bean.isnull")); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index 9d40af426..cee293377 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -127,13 +127,14 @@ public class InternalConfiguration { this.beanDescriptorManager = new BeanDescriptorManager(this); Map asOfTableMapping = beanDescriptorManager.deploy(); + Map 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()) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java index e53c735d3..3fc05528c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -119,6 +119,8 @@ public final class PersistRequestBean extends PersistRequest implements BeanP */ private boolean requestUpdateAllLoadedProps; + private boolean publish; + public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager mgr, SpiTransaction t, PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse) { @@ -736,4 +738,51 @@ public final class PersistRequestBean 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(); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java b/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java index a666f5d93..b939219d8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java @@ -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. - *

- * This will also cascade delete one level of children. - *

- */ - int delete(Class beanType, Object id, Transaction transaction); + /** + * Delete a bean given it's type and id value. + *

+ * This will also cascade delete one level of children. + *

+ */ + 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. + */ + void publish(Query 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); - } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 628bfcfa7..364ceb51f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -147,11 +147,17 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { 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 implements MetaBeanInfo, SpiBeanType { private final boolean updateChangesOnly; private final boolean cacheSharableBeans; - + + private final BeanDescriptorDraftHelp draftHelp; private final BeanDescriptorCacheHelp cacheHelp; private final BeanDescriptorJsonHelp jsonHelp; @@ -372,7 +379,10 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { 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 implements MetaBeanInfo, SpiBeanType { this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly(); this.cacheHelp = new BeanDescriptorCacheHelp(this, owner.getCacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); this.jsonHelp = new BeanDescriptorJsonHelp(this); + this.draftHelp = new BeanDescriptorDraftHelp(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 implements MetaBeanInfo, SpiBeanType { *

* @param withHistoryTables map populated if @History is supported on this entity bean */ - public void initialiseId(Map withHistoryTables) { + public void initialiseId(Map withHistoryTables, Map 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 implements MetaBeanInfo, SpiBeanType { 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 implements MetaBeanInfo, SpiBeanType { */ 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 implements MetaBeanInfo, SpiBeanType { 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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorDraftHelp.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorDraftHelp.java new file mode 100644 index 000000000..86ade6568 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorDraftHelp.java @@ -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 The entity bean type + */ +public final class BeanDescriptorDraftHelp { + + private final BeanDescriptor desc; + + public BeanDescriptorDraftHelp(BeanDescriptor 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; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java index a36703079..72c6b1a8a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -167,6 +167,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { */ private final Map asOfTableMap = new HashMap(); + /** + * Map of base tables to 'draft' tables. + */ + private final Map draftTableMap = new HashMap(); + /** * 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 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: diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 3ae1978d5..dbec4d4bf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbReadContext.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbReadContext.java index f4085c646..0581f3288 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbReadContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbReadContext.java @@ -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(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java index 2542c7c7b..1f6714c06 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java @@ -120,4 +120,9 @@ public interface DbSqlContext { */ void appendHistorySysPeriod(); + /** + * Return true if the query is a 'asDraft' query. + */ + boolean isDraftQuery(); + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 2489e75ab..c8c8ec4e7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -118,10 +118,16 @@ public class DeployBeanDescriptor { 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 { 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 { 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 { this.baseTable = baseTableFull == null ? null : baseTableFull.getQualifiedName(); this.baseTableAsOf = baseTable + asOfSuffix; this.baseTableVersionsBetween = baseTable + versionsBetweenSuffix; + this.draftTable = (draftable) ? baseTable+"_draft" : baseTable; } public void sortProperties() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 2224b0565..c87c35246 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -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; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java index c869a4339..5a6e124b5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 5cb6ad912..85683bdb6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java index db3bdb663..92ed1ea97 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java @@ -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 void publish(Query query, Transaction transaction) { + + query.asDraft(); + + Class beanType = query.getBeanType(); + List draftBeans = server.findList(query, transaction); + + BeanDescriptor desc = server.getBeanDescriptor(beanType); + + // get the list of Id's + List idList = new ArrayList(); + for (T draftBean: draftBeans) { + idList.add(desc.getBeanId(draftBean)); } + + // fetch existing live beans to update (or insert if missing) + Map liveBeans = server.find(beanType) + .where().idIn(idList) + .findMap(); + + List livePublish = new ArrayList(idList.size()); + + BeanManager 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 request = createRequest(liveBean, transaction, null, mgr, persistType, false); + request.setPublish(); + + if (persistType == Type.INSERT) { + insert(request); + } else { + update(request); + } + } + } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertHandler.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertHandler.java index c35acb0ef..1525ca479 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertHandler.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertHandler.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertMeta.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertMeta.java index 97ffd0bc5..06ab7d8eb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertMeta.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/InsertMeta.java @@ -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()); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/MetaFactory.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/MetaFactory.java index 44ad8f336..1ad53e203 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/MetaFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/MetaFactory.java @@ -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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/UpdateMeta.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/UpdateMeta.java index 7871a4284..152b25cbb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dml/UpdateMeta.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dml/UpdateMeta.java @@ -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=? diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/Bindable.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/Bindable.java index c1b3ec1e2..797b31609 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/Bindable.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/Bindable.java @@ -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(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableAssocOne.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableAssocOne.java index 1547fc8aa..fa1e53de3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableAssocOne.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableAssocOne.java @@ -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 list) { if (request.isAddToUpdate(assocOne)) { list.add(this); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableCompound.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableCompound.java index 8f631c50d..ebfb52d5c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableCompound.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableCompound.java @@ -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++) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableDiscriminator.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableDiscriminator.java index 2cd2f726d..21235b5f9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableDiscriminator.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableDiscriminator.java @@ -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 list) { throw new PersistenceException("Never called (only for inserts)"); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEmbedded.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEmbedded.java index 7d5a5d5c6..29d5b9e70 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEmbedded.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEmbedded.java @@ -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++) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEncryptedProperty.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEncryptedProperty.java index 72e736e79..4863f7e51 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEncryptedProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableEncryptedProperty.java @@ -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 list) { if (request.isAddToUpdate(prop)) { list.add(this); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmbedded.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmbedded.java index feeec637f..1fb9a0730 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmbedded.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmbedded.java @@ -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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmpty.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmpty.java index f1c434e69..3c2584a70 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmpty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdEmpty.java @@ -13,6 +13,11 @@ public class BindableIdEmpty implements BindableId { return true; } + @Override + public boolean isDraftOnly() { + return false; + } + public void addToUpdate(PersistRequestBean request, List list) { // nothing } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdScalar.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdScalar.java index a1900a9dd..2cba8c20a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdScalar.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableIdScalar.java @@ -38,6 +38,11 @@ public final class BindableIdScalar implements BindableId { return uidProp.toString(); } + @Override + public boolean isDraftOnly() { + return false; + } + /** * Does nothing for BindableId. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableList.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableList.java index 46000fae1..cf46cd141 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableList.java @@ -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 copy = new ArrayList(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 list) { for (int i = 0; i < items.length; i++) { list.add(items[i]); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableProperty.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableProperty.java index 44ac14a17..5b3b85d1b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableProperty.java @@ -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 list) { if (request.isAddToUpdate(prop)) { list.add(this); diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableUnidirectional.java b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableUnidirectional.java index 8651752b3..c980c9be6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableUnidirectional.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/dmlbind/BindableUnidirectional.java @@ -38,6 +38,11 @@ public class BindableUnidirectional implements Bindable { return "BindableShadowFKey " + unidirectional; } + @Override + public boolean isDraftOnly() { + return false; + } + public void addToUpdate(PersistRequestBean request, List list) { throw new PersistenceException("Never called (for insert only)"); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java index 3bde3c115..acfad406b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java @@ -234,6 +234,11 @@ public class CQuery implements DbReadContext, CancelableQuery { } } + @Override + public boolean isDraftQuery() { + return query.isAsDraft(); + } + public Boolean isReadOnly() { return readOnly; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 56ac874bf..c507de345 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -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 CQueryDraftSupport getDraftSupport(SpiQuery 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 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. *

*/ - 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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDraftSupport.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDraftSupport.java new file mode 100644 index 000000000..50c6e62fd --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryDraftSupport.java @@ -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 tableMap; + + public CQueryDraftSupport(Map 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); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java index 8b832230b..f3ef190f4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java @@ -41,10 +41,10 @@ public class CQueryEngine { private final CQueryHistorySupport historySupport; - public CQueryEngine(DatabasePlatform dbPlatform, Binder binder, Map asOfTableMapping, String asOfSysPeriod) { + public CQueryEngine(DatabasePlatform dbPlatform, Binder binder, Map asOfTableMapping, String asOfSysPeriod, Map 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 CQuery buildQuery(OrmQueryRequest request) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java index e359cb3b2..b48d0aa0c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryFetchIds.java @@ -266,6 +266,10 @@ public class CQueryFetchIds { // no-op } + @Override + public boolean isDraftQuery() { + return false; + } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java index 9eed4b615..8b5795136 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -45,6 +45,8 @@ public class DefaultDbSqlContext implements DbSqlContext { private ArrayList 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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlBeanLoad.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlBeanLoad.java index c6b81022b..c6395dd37 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlBeanLoad.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlBeanLoad.java @@ -53,7 +53,7 @@ public class SqlBeanLoad { public Object load(BeanProperty prop) { - if (!rawSql && !prop.isLoadProperty()) { + if (!rawSql && !prop.isLoadProperty(ctx.isDraftQuery())) { return null; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java index 68304fffe..4625f6919 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java @@ -98,7 +98,7 @@ public class SqlTreeBuilder { * to the root node. */ public SqlTreeBuilder(String tableAliasPlaceHolder, String columnAliasPrefix, - OrmQueryRequest request, CQueryPredicates predicates, 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); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index e509c2205..2017045e0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -310,6 +310,11 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + public DefaultOrmQuery asDraft() { + this.temporalMode = TemporalMode.DRAFT; + return this; + } + /** * Set the BeanDescriptor for the root type of this query. */ @@ -647,6 +652,11 @@ public class DefaultOrmQuery implements SpiQuery { 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 implements SpiQuery { 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); diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 22babcee4..b8c35da23 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -347,6 +347,16 @@ public class TDSpiEbeanServer implements SpiEbeanServer { } + @Override + public void publish(Class beanType, Object id, Transaction transaction) { + + } + + @Override + public void publish(Query query, Transaction transaction) { + + } + @Override public Transaction createTransaction() { return null;