This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit e08691b901026e050f7089e7288525554f54e6f0 Author: Andrus Adamchik <[email protected]> AuthorDate: Fri Jul 3 11:43:08 2026 -0400 CAY-2969 Extender API for "soft" delete implementing soft delete in the stack instead of intercepting --- RELEASE-NOTES.txt | 1 + .../dbsync/reverse/configuration/ToolsModule.java | 3 + .../cayenne/access/flush/DbRowOpFactory.java | 41 ++--- .../access/flush/DefaultDataDomainFlushAction.java | 8 +- .../flush/DefaultDataDomainFlushActionFactory.java | 6 +- .../cayenne/access/flush/QueryCreatorVisitor.java | 45 ++++-- .../ConditionalSoftDeleteDbRowOpFactory.java | 53 +++++++ .../access/flush/operation/DbRowOpVisitor.java | 10 ++ ...RowOpVisitor.java => DeleteDbRowOpFactory.java} | 30 ++-- .../flush/operation/DeleteInsertDbRowOp.java | 5 +- ...pVisitor.java => HardDeleteDbRowOpFactory.java} | 24 +-- .../access/flush/operation/SoftDeleteDbRowOp.java | 69 ++++++++ .../batch/SoftDeleteBatchTranslator.java | 104 ------------- .../cayenne/configuration/runtime/CoreModule.java | 3 + .../configuration/runtime/CoreModuleExtender.java | 9 +- .../access/DefaultDataRowStoreFactoryIT.java | 3 + .../org/apache/cayenne/access/SoftDeleteIT.java | 23 +++ .../batch/SoftDeleteBatchTranslatorIT.java | 173 --------------------- .../runtime/DataContextFactoryTest.java | 4 + 19 files changed, 267 insertions(+), 347 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 40cb8de23..fa434092c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -18,6 +18,7 @@ CAY-2956 Get rid of a dedicated adapter for Oracle 8 CAY-2957 Get rid of adapter for legacy HSQLDB <= 1.8 CAY-2962 Allow unconstrained VARCHAR CAY-2963 Replace TypesHandler / types.xml with hardcoded map +CAY-2969 Extender API for "soft" delete Bug Fixes: diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java index 1515f5c14..80e2e0386 100644 --- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java +++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java @@ -23,6 +23,8 @@ import org.apache.cayenne.access.flush.DataDomainFlushActionFactory; import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter; +import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.access.translator.batch.BatchTranslator; import org.apache.cayenne.access.translator.batch.DeleteBatchTranslator; import org.apache.cayenne.access.translator.batch.InsertBatchTranslator; @@ -172,6 +174,7 @@ public class ToolsModule implements Module { binder.bind(XMLReader.class).toProviderInstance(new XMLReaderProvider(true)).withoutScope(); binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class); binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class); + binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java index accf3295d..fd418f8f6 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java @@ -19,12 +19,6 @@ package org.apache.cayenne.access.flush; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.ObjectId; import org.apache.cayenne.Persistent; @@ -32,15 +26,19 @@ import org.apache.cayenne.access.ObjectDiff; import org.apache.cayenne.access.ObjectStore; import org.apache.cayenne.access.flush.operation.DbRowOp; import org.apache.cayenne.access.flush.operation.DbRowOpType; -import org.apache.cayenne.access.flush.operation.DeleteDbRowOp; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.access.flush.operation.InsertDbRowOp; import org.apache.cayenne.access.flush.operation.UpdateDbRowOp; -import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.EntityResolver; -import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.reflect.ClassDescriptor; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + /** * Factory that produces a collection of {@link DbRowOp} from given {@link ObjectDiff}. * @@ -53,21 +51,27 @@ class DbRowOpFactory { private final Set<ArcTarget> processedArcs; private final Map<ObjectId, DbRowOp> dbRows; private final RootRowOpProcessor rootRowOpProcessor; + private final DeleteDbRowOpFactory deleteDbRowOpFactory; private ClassDescriptor descriptor; private Persistent object; private ObjectDiff diff; - DbRowOpFactory(EntityResolver resolver, ObjectStore store, Set<ArcTarget> processedArcs) { + DbRowOpFactory(EntityResolver resolver, + ObjectStore store, + Set<ArcTarget> processedArcs, + DeleteDbRowOpFactory deleteDbRowOpFactory) { + this.resolver = resolver; this.store = store; this.dbRows = new HashMap<>(4); this.processedArcs = processedArcs; this.rootRowOpProcessor = new RootRowOpProcessor(this); + this.deleteDbRowOpFactory = deleteDbRowOpFactory; } private void updateDiff(ObjectDiff diff) { - ObjectId id = (ObjectId)diff.getNodeId(); + ObjectId id = (ObjectId) diff.getNodeId(); this.diff = diff; this.descriptor = resolver.getClassDescriptor(id.getEntityName()); this.object = (Persistent) store.getNode(id); @@ -95,7 +99,7 @@ class DbRowOpFactory { private DbRowOp createRow(DbEntity entity, ObjectId id, DbRowOpType type) { // skip phantom nodes, this could be a created and immediately deleted relationship - if(store.getNode(id) == null && !id.getEntityName().startsWith("db:")) { + if (store.getNode(id) == null && !id.getEntityName().startsWith("db:")) { return null; } switch (type) { @@ -104,7 +108,7 @@ class DbRowOpFactory { case UPDATE: return new UpdateDbRowOp(object, entity, id); case DELETE: - return new DeleteDbRowOp(object, entity, id); + return deleteDbRowOpFactory.createOp(object, entity, id); } throw new CayenneRuntimeException("Unknown DbRowType '%s'", type); } @@ -125,17 +129,6 @@ class DbRowOpFactory { return diff; } - DbEntity getDbEntity(ObjectId id) { - String entityName = id.getEntityName(); - if(entityName.startsWith(ASTDbPath.DB_PREFIX)) { - entityName = entityName.substring(ASTDbPath.DB_PREFIX.length()); - return resolver.getDbEntity(entityName); - } else { - ObjEntity objEntity = resolver.getObjEntity(entityName); - return objEntity.getDbEntity(); - } - } - Set<ArcTarget> getProcessedArcs() { return processedArcs; } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java index d67ed20be..d4c3de940 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java @@ -42,6 +42,7 @@ import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.operation.DbRowOp; import org.apache.cayenne.access.flush.operation.DbRowOpVisitor; import org.apache.cayenne.access.flush.operation.DeleteDbRowOp; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.access.flush.operation.InsertDbRowOp; import org.apache.cayenne.access.flush.operation.OpIdFactory; import org.apache.cayenne.access.flush.operation.UpdateDbRowOp; @@ -62,12 +63,15 @@ public class DefaultDataDomainFlushAction implements DataDomainFlushAction { protected final DbRowOpSorter dbRowOpSorter; protected final JdbcEventLogger jdbcEventLogger; protected final OperationObserver observer; + protected final DeleteDbRowOpFactory deleteDbRowOpFactory; - protected DefaultDataDomainFlushAction(DataDomain dataDomain, DbRowOpSorter dbRowOpSorter, JdbcEventLogger jdbcEventLogger) { + protected DefaultDataDomainFlushAction(DataDomain dataDomain, DbRowOpSorter dbRowOpSorter, + JdbcEventLogger jdbcEventLogger, DeleteDbRowOpFactory deleteDbRowOpFactory) { this.dataDomain = dataDomain; this.dbRowOpSorter = dbRowOpSorter; this.jdbcEventLogger = jdbcEventLogger; this.observer = new FlushObserver(jdbcEventLogger); + this.deleteDbRowOpFactory = deleteDbRowOpFactory; } @Override @@ -111,7 +115,7 @@ public class DefaultDataDomainFlushAction implements DataDomainFlushAction { List<DbRowOp> ops = new ArrayList<>(changesByObjectId.size()); Set<ArcTarget> processedArcs = new HashSet<>(); - DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, processedArcs); + DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, processedArcs, deleteDbRowOpFactory); // ops.addAll() method is slower in this case as it will allocate new array for all values //noinspection UseBulkOperation changesByObjectId.forEach((obj, diff) -> factory.createRows(diff).forEach(ops::add)); diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java index ad260b1cd..299f79875 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java @@ -21,6 +21,7 @@ package org.apache.cayenne.access.flush; import org.apache.cayenne.access.DataDomain; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.di.Inject; import org.apache.cayenne.log.JdbcEventLogger; @@ -37,8 +38,11 @@ public class DefaultDataDomainFlushActionFactory implements DataDomainFlushActio @Inject private JdbcEventLogger jdbcEventLogger; + @Inject + private DeleteDbRowOpFactory deleteDbRowOpFactory; + @Override public DataDomainFlushAction createFlushAction(DataDomain dataDomain) { - return new DefaultDataDomainFlushAction(dataDomain, operationSorter, jdbcEventLogger); + return new DefaultDataDomainFlushAction(dataDomain, operationSorter, jdbcEventLogger, deleteDbRowOpFactory); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java index 3f7a55f2c..1ac359b77 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java @@ -19,19 +19,20 @@ package org.apache.cayenne.access.flush; -import java.util.ArrayList; -import java.util.List; - import org.apache.cayenne.access.flush.operation.DbRowOp; import org.apache.cayenne.access.flush.operation.DbRowOpVisitor; import org.apache.cayenne.access.flush.operation.DeleteDbRowOp; import org.apache.cayenne.access.flush.operation.InsertDbRowOp; +import org.apache.cayenne.access.flush.operation.SoftDeleteDbRowOp; import org.apache.cayenne.access.flush.operation.UpdateDbRowOp; import org.apache.cayenne.query.BatchQuery; import org.apache.cayenne.query.DeleteBatchQuery; import org.apache.cayenne.query.InsertBatchQuery; import org.apache.cayenne.query.UpdateBatchQuery; +import java.util.ArrayList; +import java.util.List; + /** * Visitor that creates batch queries. * It relies on correct sorting of {@link DbRowOp} to just linearly scan of rows and put them in batches. @@ -59,12 +60,12 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> { @Override public Void visitInsert(InsertDbRowOp dbRow) { InsertBatchQuery query; - if(lastRow == null || !lastRow.isSameBatch(dbRow)) { + if (lastRow == null || !lastRow.isSameBatch(dbRow)) { query = new InsertBatchQuery(dbRow.getEntity(), batchSize); queryList.add(query); lastBatch = query; } else { - query = (InsertBatchQuery)lastBatch; + query = (InsertBatchQuery) lastBatch; } query.add(dbRow.getValues().getSnapshot(), dbRow.getChangeId()); lastRow = dbRow; @@ -74,12 +75,35 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> { @Override public Void visitUpdate(UpdateDbRowOp dbRow) { // skip empty update.. - if(dbRow.getValues().isEmpty()) { + if (dbRow.getValues().isEmpty()) { return null; } UpdateBatchQuery query; - if(lastRow == null || !lastRow.isSameBatch(dbRow)) { + if (lastRow == null || !lastRow.isSameBatch(dbRow)) { + query = new UpdateBatchQuery( + dbRow.getEntity(), + dbRow.getQualifier().getQualifierAttributes(), + dbRow.getValues().getUpdatedAttributes(), + dbRow.getQualifier().getNullQualifierNames(), + batchSize + ); + query.setUsingOptimisticLocking(dbRow.getQualifier().isUsingOptimisticLocking()); + queryList.add(query); + lastBatch = query; + } else { + query = (UpdateBatchQuery) lastBatch; + } + query.add(dbRow.getQualifier().getSnapshot(), dbRow.getValues().getSnapshot(), dbRow.getChangeId()); + lastRow = dbRow; + return null; + } + + @Override + public Void visitSoftDelete(SoftDeleteDbRowOp dbRow) { + // a soft delete is executed as an update of the soft-delete flag column + UpdateBatchQuery query; + if (lastRow == null || !lastRow.isSameBatch(dbRow)) { query = new UpdateBatchQuery( dbRow.getEntity(), dbRow.getQualifier().getQualifierAttributes(), @@ -91,7 +115,8 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> { queryList.add(query); lastBatch = query; } else { - query = (UpdateBatchQuery)lastBatch; + // dbRow.isSameBatch() guarantees the last batch is an update created by this method + query = (UpdateBatchQuery) lastBatch; } query.add(dbRow.getQualifier().getSnapshot(), dbRow.getValues().getSnapshot(), dbRow.getChangeId()); lastRow = dbRow; @@ -101,7 +126,7 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> { @Override public Void visitDelete(DeleteDbRowOp dbRow) { DeleteBatchQuery query; - if(lastRow == null || !lastRow.isSameBatch(dbRow)) { + if (lastRow == null || !lastRow.isSameBatch(dbRow)) { query = new DeleteBatchQuery( dbRow.getEntity(), dbRow.getQualifier().getQualifierAttributes(), @@ -112,7 +137,7 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> { queryList.add(query); lastBatch = query; } else { - query = (DeleteBatchQuery)lastBatch; + query = (DeleteBatchQuery) lastBatch; } query.add(dbRow.getQualifier().getSnapshot()); lastRow = dbRow; diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java new file mode 100644 index 000000000..90be083ad --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java @@ -0,0 +1,53 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ + +package org.apache.cayenne.access.flush.operation; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; + +import java.sql.Types; +import java.util.Objects; + +/** + * A {@link DeleteDbRowOpFactory} enabling "soft delete". For tables that contain a BOOLEAN column with the configured + * name it produces {@link SoftDeleteDbRowOp}, so the row is deleted by an UPDATE setting that column to {@code true}. + * Tables without such a column (or whose column of that name is not BOOLEAN) get a plain {@link DeleteDbRowOp} and + * are deleted with a regular SQL DELETE. + * + * @since 5.0 + */ +public class ConditionalSoftDeleteDbRowOpFactory implements DeleteDbRowOpFactory { + + private final String columnName; + + public ConditionalSoftDeleteDbRowOpFactory(String columnName) { + this.columnName = Objects.requireNonNull(columnName); + } + + @Override + public DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId id) { + DbAttribute attr = entity.getAttribute(columnName); + return attr != null && attr.getType() == Types.BOOLEAN + ? new SoftDeleteDbRowOp(object, entity, id, attr) + : new DeleteDbRowOp(object, entity, id); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java index 25a110396..ea432c179 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java @@ -35,4 +35,14 @@ public interface DbRowOpVisitor<T> { default T visitDelete(DeleteDbRowOp dbRow) { return null; } + + /** + * Visits a "soft" delete op. The default implementation delegates to {@link #visitDelete(DeleteDbRowOp)}, so + * visitors that don't distinguish soft deletes from regular ones automatically get correct delete semantics. + * + * @since 5.0 + */ + default T visitSoftDelete(SoftDeleteDbRowOp dbRow) { + return visitDelete(dbRow); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java similarity index 62% copy from cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java copy to cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java index 25a110396..661616078 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java @@ -19,20 +19,24 @@ package org.apache.cayenne.access.flush.operation; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.map.DbEntity; + /** - * @since 4.2 + * A factory of delete row ops used by the commit pipeline. + * + * @since 5.0 */ -public interface DbRowOpVisitor<T> { - - default T visitInsert(InsertDbRowOp dbRow) { - return null; - } - - default T visitUpdate(UpdateDbRowOp dbRow) { - return null; - } +public interface DeleteDbRowOpFactory { - default T visitDelete(DeleteDbRowOp dbRow) { - return null; - } + /** + * Creates a delete op for a given object and target table. + * + * @param object the object being deleted + * @param entity the target DbEntity (may be the root table or an additional / flattened table) + * @param id the id of the row being deleted + * @return a delete op + */ + DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId id); } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java index da6195b96..6394d04d8 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java @@ -38,8 +38,9 @@ public class DeleteInsertDbRowOp extends BaseDbRowOp { @Override public <T> T accept(DbRowOpVisitor<T> visitor) { - visitor.visitDelete(delete); - return visitor.visitInsert(insert); + // double-dispatch through the wrapped ops, so their subclasses (e.g. SoftDeleteDbRowOp) route correctly + delete.accept(visitor); + return insert.accept(visitor); } @Override diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java similarity index 67% copy from cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java copy to cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java index 25a110396..a80973f91 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java @@ -19,20 +19,20 @@ package org.apache.cayenne.access.flush.operation; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.map.DbEntity; + /** - * @since 4.2 + * Default implementation of {@link DeleteDbRowOpFactory} producing plain {@link DeleteDbRowOp} that translate to SQL + * DELETE. + * + * @since 5.0 */ -public interface DbRowOpVisitor<T> { - - default T visitInsert(InsertDbRowOp dbRow) { - return null; - } - - default T visitUpdate(UpdateDbRowOp dbRow) { - return null; - } +public class HardDeleteDbRowOpFactory implements DeleteDbRowOpFactory { - default T visitDelete(DeleteDbRowOp dbRow) { - return null; + @Override + public DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId id) { + return new DeleteDbRowOp(object, entity, id); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java new file mode 100644 index 000000000..3b281ba6f --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java @@ -0,0 +1,69 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ + +package org.apache.cayenne.access.flush.operation; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; + +/** + * A "soft" delete op that is executed as an UPDATE setting the soft-delete flag column to {@code true} instead of a + * SQL DELETE, leaving the row physically in the table. For all other purposes — sorting, merging, snapshot cache + * eviction and the rest of the flush postprocessing — it behaves exactly like a {@link DeleteDbRowOp}. + * + * @since 5.0 + */ +public class SoftDeleteDbRowOp extends DeleteDbRowOp implements DbRowOpWithValues { + + protected final Values values; + + public SoftDeleteDbRowOp(Persistent object, DbEntity entity, ObjectId id, DbAttribute deletedAttribute) { + super(object, entity, id); + this.values = new Values(this, false); + values.addValue(deletedAttribute, true, false); + } + + @Override + public <T> T accept(DbRowOpVisitor<T> visitor) { + return visitor.visitSoftDelete(this); + } + + @Override + public Values getValues() { + return values; + } + + @Override + public boolean isSameBatch(DbRowOp rowOp) { + if (!(rowOp instanceof SoftDeleteDbRowOp other)) { + return false; + } + if (!super.isSameBatch(rowOp)) { + return false; + } + return values.isSameBatch(other.values); + } + + @Override + public String toString() { + return "soft " + super.toString(); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java deleted file mode 100644 index a52f597b7..000000000 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java +++ /dev/null @@ -1,104 +0,0 @@ -/***************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - ****************************************************************/ - -package org.apache.cayenne.access.translator.batch; - -import org.apache.cayenne.access.jdbc.PSBatchParameter; -import org.apache.cayenne.access.sqlbuilder.SQLBuilder; -import org.apache.cayenne.access.sqlbuilder.UpdateBuilder; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.types.ExtendedType; -import org.apache.cayenne.dba.TypesMapping; -import org.apache.cayenne.map.DbAttribute; -import org.apache.cayenne.query.BatchQueryRow; -import org.apache.cayenne.query.DeleteBatchQuery; - -import java.sql.Types; - -import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*; - -/** - * A delete {@link BatchTranslator} that performs a 'soft' delete (an UPDATE setting the 'deleted' field - * to true) for entities that have a boolean 'deleted' field, and falls back to the regular SQL DELETE of - * the {@link DeleteBatchTranslator} superclass for the rest. Bind it under the {@link BatchTranslator#DELETE} - * name to enable soft deletes. The preferred way to do this is - * {@code CoreModule.extend(binder).useSoftDeleteIfColumnPresent(columnName)} (see - * {@link org.apache.cayenne.configuration.runtime.CoreModuleExtender#useSoftDeleteIfColumnPresent(String)}). - * - * @since 4.2 - */ -public class SoftDeleteBatchTranslator extends DeleteBatchTranslator { - - public static final String DEFAULT_DELETED_FIELD_NAME = "DELETED"; - - private final String deletedFieldName; - - public SoftDeleteBatchTranslator() { - this(DEFAULT_DELETED_FIELD_NAME); - } - - public SoftDeleteBatchTranslator(String deletedFieldName) { - this.deletedFieldName = deletedFieldName; - } - - protected boolean isHardDelete(DeleteBatchQuery query) { - DbAttribute attr = query.getDbEntity().getAttribute(deletedFieldName); - return attr == null || attr.getType() != Types.BOOLEAN; - } - - @Override - protected String createSql(BatchTranslatorContext<DeleteBatchQuery> context) { - DeleteBatchQuery query = context.getQuery(); - if (isHardDelete(query)) { - return super.createSql(context); - } - - DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName); - UpdateBuilder updateBuilder = update(context.getRootDbEntity()) - .set(column(deletedFieldName).attribute(deleteAttribute) - .eq(SQLBuilder.value(true).attribute(deleteAttribute))) - .where(buildQualifier(context, query.getDbAttributes())); - - return doTranslate(context, updateBuilder); - } - - @Override - protected PSParameter[] updateBindings(BatchTranslatorContext<DeleteBatchQuery> context, - PSBatchParameter[] template, BatchQueryRow row) { - DeleteBatchQuery deleteBatch = context.getQuery(); - if (isHardDelete(deleteBatch)) { - return super.updateBindings(context, template, row); - } - - PSParameter[] bindings = new PSParameter[template.length]; - - // the 'deleted' flag is the first binding and stays constant across all rows of the batch - DbAttribute deleteAttribute = deleteBatch.getDbEntity().getAttribute(deletedFieldName); - String typeName = TypesMapping.getJavaBySqlType(deleteAttribute); - ExtendedType extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(typeName); - bindings[0] = template[0].bind(Boolean.TRUE, 1, extendedType); - - // bindings[0] holds the constant 'deleted' flag, so qualifier values start at position 1 - for(int i=0, position=1; i<deleteBatch.getDbAttributes().size(); i++) { - position = updateBinding(context, template, bindings, row.getValue(i), position); - } - - return bindings; - } -} diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java index 94f1519f0..4a561aba8 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java @@ -35,6 +35,8 @@ import org.apache.cayenne.access.flush.DataDomainFlushActionFactory; import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter; +import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.access.translator.sqltemplate.SQLTemplateTranslator; import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory; import org.apache.cayenne.access.jdbc.reader.RowReaderFactory; @@ -494,6 +496,7 @@ public class CoreModule implements Module { binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class); binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class); + binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class); binder.bind(CommitLogFilter.class).to(CommitLogFilter.class); binder.bind(CommitLogEntityFactory.class).to(IncludeAllCommitLogEntityFactory.class); diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java index cc64c09b1..e27758132 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java @@ -21,8 +21,8 @@ package org.apache.cayenne.configuration.runtime; import org.apache.cayenne.DataChannelQueryFilter; import org.apache.cayenne.DataChannelSyncFilter; -import org.apache.cayenne.access.translator.batch.BatchTranslator; -import org.apache.cayenne.access.translator.batch.SoftDeleteBatchTranslator; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; +import org.apache.cayenne.access.flush.operation.ConditionalSoftDeleteDbRowOpFactory; import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.access.types.ExtendedTypeFactory; import org.apache.cayenne.access.types.ValueObjectType; @@ -34,7 +34,6 @@ import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.PkGenerator; import org.apache.cayenne.di.Binder; -import org.apache.cayenne.di.Key; import org.apache.cayenne.di.ListBuilder; import org.apache.cayenne.di.MapBuilder; import org.apache.cayenne.graph.GraphChangeHandler; @@ -118,12 +117,10 @@ public class CoreModuleExtender { * of the mapping, e.g. a DbEntity qualifier like {@code DELETED = false or DELETED = null}. * * @param columnName the name of the BOOLEAN column marking soft-deleted rows - * @see SoftDeleteBatchTranslator * @since 5.0 */ public CoreModuleExtender useSoftDeleteIfColumnPresent(String columnName) { - binder.bind(Key.get(BatchTranslator.class, BatchTranslator.DELETE)) - .toInstance(new SoftDeleteBatchTranslator(columnName)); + binder.bind(DeleteDbRowOpFactory.class).toInstance(new ConditionalSoftDeleteDbRowOpFactory(columnName)); return this; } diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java index ad3cb0285..d18e5148a 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java @@ -23,6 +23,8 @@ import org.apache.cayenne.access.flush.DataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter; +import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.configuration.DefaultRuntimeProperties; import org.apache.cayenne.configuration.RuntimeProperties; import org.apache.cayenne.configuration.runtime.CoreModule; @@ -115,6 +117,7 @@ public class DefaultDataRowStoreFactoryIT { binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class); binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class); binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class); + binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class); binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class); binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class); new TestExtender(binder).initAllExtensions(); diff --git a/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java index 863ab1462..0c681611e 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java @@ -140,6 +140,29 @@ public class SoftDeleteIT { assertEquals(0, ObjectSelect.query(HardDelete.class).selectCount(context)); } + @Test + public void mixedSoftAndHardDeleteInOneCommit() throws Exception { + TableHelper tHardDelete = env.table("HARD_DELETE", "ID", "NAME"); + DataContext context = env.context(); + + SoftDelete soft = context.newObject(SoftDelete.class); + soft.setName("soft"); + HardDelete hard = context.newObject(HardDelete.class); + hard.setName("hard"); + context.commitChanges(); + + context.deleteObjects(soft, hard); + context.commitChanges(); + + // the SoftDelete row survives, flagged as deleted, and is hidden by the qualifier + assertEquals(1, tSoftDelete.getRowCount()); + assertTrue(toBoolean(tSoftDelete.selectAll().getFirst()[2]), "DELETED flag should be true"); + assertEquals(0, ObjectSelect.query(SoftDelete.class).selectCount(context)); + + // the HardDelete row is physically gone + assertEquals(0, tHardDelete.getRowCount()); + } + private static boolean toBoolean(Object value) { if (value instanceof Boolean b) { return b; diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java deleted file mode 100644 index b8ecf8423..000000000 --- a/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java +++ /dev/null @@ -1,173 +0,0 @@ -/***************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - ****************************************************************/ -package org.apache.cayenne.access.translator.batch; - -import org.apache.cayenne.PersistenceState; -import org.apache.cayenne.access.DataNode; -import org.apache.cayenne.dba.DbAdapter; -import org.apache.cayenne.dba.JdbcAdapter; -import org.apache.cayenne.di.AdhocObjectFactory; -import org.apache.cayenne.exp.Expression; -import org.apache.cayenne.exp.ExpressionFactory; -import org.apache.cayenne.map.DbAttribute; -import org.apache.cayenne.map.DbEntity; -import org.apache.cayenne.query.DeleteBatchQuery; -import org.apache.cayenne.query.ObjectSelect; -import org.apache.cayenne.query.SQLTemplate; -import org.apache.cayenne.test.parallel.ParallelTestContainer; -import org.apache.cayenne.testdo.soft_delete.SoftDelete; -import org.apache.cayenne.unit.dba.TestDbAdapter; -import org.apache.cayenne.unit.CayenneProjects; -import org.apache.cayenne.unit.CayenneTestsEnv; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class SoftDeleteBatchTranslatorIT { - - @RegisterExtension - static final CayenneTestsEnv env = CayenneTestsEnv.forProject(CayenneProjects.SOFT_DELETE_PROJECT); - - private DataNode node; - private TestDbAdapter unitAdapter; - private AdhocObjectFactory objectFactory; - - private String createSql(DeleteBatchQuery query) { - DbAdapter adapter = objectFactory.newInstance(JdbcAdapter.class, JdbcAdapter.class.getName()); - return createSql(query, adapter); - } - - private String createSql(DeleteBatchQuery query, DbAdapter adapter) { - return new SoftDeleteBatchTranslator().translate(query, adapter).sql(); - } - - @BeforeEach - public void setUp() { - node = env.dataNode(); - unitAdapter = env.testDbAdapter(); - objectFactory = env.adhocObjectFactory(); - } - - @Test - public void createSqlString() { - DbEntity entity = env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity(); - - List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("ID")); - - DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1); - String generatedSql = createSql(deleteQuery); - assertNotNull(generatedSql); - assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ID = ?", generatedSql); - } - - @Test - public void createSqlStringWithNulls() { - DbEntity entity = env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity(); - - List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("ID"), entity.getAttribute("NAME")); - - Collection<String> nullAttributes = Collections.singleton("NAME"); - - DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, nullAttributes, 1); - String generatedSql = createSql(deleteQuery); - assertNotNull(generatedSql); - assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ( ID = ? ) AND ( NAME IS NULL )", generatedSql); - } - - @Test - public void createSqlStringWithIdentifiersQuote() { - DbEntity entity = env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity(); - try { - - entity.getDataMap().setQuotingSQLIdentifiers(true); - - List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("ID")); - - DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1); - DbAdapter adapter = node.getAdapter(); - String generatedSql = createSql(deleteQuery, adapter); - - String charStart = unitAdapter.getIdentifiersStartQuote(); - String charEnd = unitAdapter.getIdentifiersEndQuote(); - - assertNotNull(generatedSql); - assertEquals("UPDATE " + charStart + entity.getName() + charEnd + " SET " + charStart + "DELETED" + charEnd - + " = ? WHERE " + charStart + "ID" + charEnd + " = ?", generatedSql); - } finally { - entity.getDataMap().setQuotingSQLIdentifiers(false); - } - - } - - @Test - public void update() throws Exception { - - DbEntity entity = env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity(); - - BatchTranslator<DeleteBatchQuery> oldTranslator = node.getDeleteBatchTranslator(); - try { - node.setDeleteBatchTranslator(new SoftDeleteBatchTranslator()); - - final SoftDelete test = env.context().newObject(SoftDelete.class); - test.setName("SoftDeleteBatchQueryBuilderTest"); - env.context().commitChanges(); - - new ParallelTestContainer() { - - @Override - protected void assertResult() { - Expression exp = ExpressionFactory.matchExp("name", test.getName()); - assertEquals(1, ObjectSelect.query(SoftDelete.class, exp).selectCount(env.context())); - - exp = ExpressionFactory.matchDbExp("DELETED", true); - assertEquals(0, ObjectSelect.query(SoftDelete.class, exp).selectCount(env.context())); - } - }.runTest(200); - - env.context().deleteObjects(test); - assertEquals(test.getPersistenceState(), PersistenceState.DELETED); - env.context().commitChanges(); - - new ParallelTestContainer() { - - @Override - protected void assertResult() { - Expression exp = ExpressionFactory.matchExp("name", test.getName()); - assertEquals(0, ObjectSelect.query(SoftDelete.class, exp).selectCount(env.context())); - - SQLTemplate template = new SQLTemplate(entity, "SELECT * FROM SOFT_DELETE"); - template.setFetchingDataRows(true); - assertEquals(1, env.context().performQuery(template).size()); - } - }.runTest(200); - } finally { - env.context().performQuery(new SQLTemplate(entity, "DELETE FROM SOFT_DELETE")); - node.setDeleteBatchTranslator(oldTranslator); - } - } - -} diff --git a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java index 7242bc1c0..db1b5f26a 100644 --- a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java +++ b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java @@ -28,6 +28,8 @@ import org.apache.cayenne.access.flush.DataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory; import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter; +import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory; import org.apache.cayenne.ashwood.AshwoodEntitySorter; import org.apache.cayenne.cache.MapQueryCache; import org.apache.cayenne.cache.QueryCache; @@ -86,6 +88,7 @@ public class DataContextFactoryTest { binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class); binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class); binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class); + binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class); binder.bind(EntitySorter.class).to(AshwoodEntitySorter.class); binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class); binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class); @@ -127,6 +130,7 @@ public class DataContextFactoryTest { binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class); binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class); binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class); + binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class); binder.bind(EntitySorter.class).to(AshwoodEntitySorter.class); binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class); binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);
