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 bc559a78d37d9d29d55e5740e0e572b0dd778814 Author: Andrus Adamchik <[email protected]> AuthorDate: Fri Jul 3 11:21:50 2026 -0400 A real test for soft delete --- .../org/apache/cayenne/access/SoftDeleteIT.java | 152 +++++++++++++++++++++ .../{SoftDelete.java => HardDelete.java} | 9 +- .../cayenne/testdo/soft_delete/SoftDelete.java | 5 - .../auto/{_SoftDelete.java => _HardDelete.java} | 32 +---- .../testdo/soft_delete/auto/_SoftDelete.java | 2 - cayenne/src/test/resources/soft-delete.map.xml | 10 +- 6 files changed, 167 insertions(+), 43 deletions(-) diff --git a/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java new file mode 100644 index 000000000..18e175d81 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java @@ -0,0 +1,152 @@ +/***************************************************************** + * 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; + +import org.apache.cayenne.PersistenceState; +import org.apache.cayenne.access.translator.batch.BatchTranslator; +import org.apache.cayenne.access.translator.batch.SoftDeleteBatchTranslator; +import org.apache.cayenne.di.Key; +import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.soft_delete.HardDelete; +import org.apache.cayenne.testdo.soft_delete.SoftDelete; +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.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class SoftDeleteIT { + + @RegisterExtension + static final CayenneTestsEnv env = CayenneTestsEnv + .forProject(CayenneProjects.SOFT_DELETE_PROJECT) + .withExtraModules(b -> b.bind(Key.get(BatchTranslator.class, BatchTranslator.DELETE)).toInstance(new SoftDeleteBatchTranslator())); + + private TableHelper tSoftDelete; + + @BeforeEach + public void setUp() { + tSoftDelete = env.table("SOFT_DELETE", "ID", "NAME", "DELETED"); + } + + @Test + public void softDeleteLeavesRowFlagged() throws Exception { + DataContext context = env.context(); + + SoftDelete object = context.newObject(SoftDelete.class); + object.setName("a"); + context.commitChanges(); + + assertEquals(1, tSoftDelete.getRowCount()); + assertNull(tSoftDelete.selectAll().getFirst()[2], "freshly inserted row should have NULL DELETED"); + + assertEquals(1, ObjectSelect.query(SoftDelete.class).selectCount(context)); + + context.deleteObjects(object); + context.commitChanges(); + + // object graph is unaware of the underlying UPDATE and treats the delete as final + assertEquals(PersistenceState.TRANSIENT, object.getPersistenceState()); + + // the row physically survives, now flagged as deleted + assertEquals(1, tSoftDelete.getRowCount()); + List<Object[]> rows = tSoftDelete.selectAll(); + assertTrue(toBoolean(rows.getFirst()[2]), "DELETED flag should be true"); + + // the entity qualifier hides the soft-deleted row + assertEquals(0, ObjectSelect.query(SoftDelete.class).selectCount(context)); + } + + @Test + public void batchSoftDeleteLeavesRowsFlagged() throws Exception { + DataContext context = env.context(); + + SoftDelete o1 = context.newObject(SoftDelete.class); + o1.setName("a"); + SoftDelete o2 = context.newObject(SoftDelete.class); + o2.setName("b"); + SoftDelete o3 = context.newObject(SoftDelete.class); + o3.setName("c"); + context.commitChanges(); + + context.deleteObjects(o1, o2, o3); + context.commitChanges(); + + assertEquals(3, tSoftDelete.getRowCount()); + for (Object[] row : tSoftDelete.selectAll()) { + assertTrue(toBoolean(row[2]), "DELETED flag should be true"); + } + + assertEquals(0, ObjectSelect.query(SoftDelete.class).selectCount(context)); + } + + @Test + public void softDeleteHidesOnlyDeletedRow() throws Exception { + DataContext context = env.context(); + + SoftDelete survivor = context.newObject(SoftDelete.class); + survivor.setName("survivor"); + SoftDelete victim = context.newObject(SoftDelete.class); + victim.setName("victim"); + context.commitChanges(); + + context.deleteObjects(victim); + context.commitChanges(); + + // both rows are still physically present + assertEquals(2, tSoftDelete.getRowCount()); + + // but the qualifier hides the soft-deleted one, leaving only the survivor visible + List<SoftDelete> visible = ObjectSelect.query(SoftDelete.class).select(context); + assertEquals(1, visible.size()); + assertEquals("survivor", visible.getFirst().getName()); + } + + @Test + public void hardDeleteFallbackRemovesRow() throws Exception { + TableHelper tHardDelete = env.table("HARD_DELETE", "ID", "NAME"); + DataContext context = env.context(); + + HardDelete object = context.newObject(HardDelete.class); + object.setName("a"); + context.commitChanges(); + + assertEquals(1, tHardDelete.getRowCount()); + + context.deleteObjects(object); + context.commitChanges(); + + // no DELETED column -> the translator falls back to a real DELETE and the row is gone + assertEquals(0, tHardDelete.getRowCount()); + assertEquals(0, ObjectSelect.query(HardDelete.class).selectCount(context)); + } + + private static boolean toBoolean(Object value) { + if (value instanceof Boolean b) { + return b; + } + // NULL means "not deleted"; some databases store BOOLEAN as a numeric flag + return value instanceof Number n && n.intValue() != 0; + } +} diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/HardDelete.java similarity index 83% copy from cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java copy to cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/HardDelete.java index 17b70a811..3c856100b 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/HardDelete.java @@ -18,13 +18,8 @@ ****************************************************************/ package org.apache.cayenne.testdo.soft_delete; -import org.apache.cayenne.testdo.soft_delete.auto._SoftDelete; +import org.apache.cayenne.testdo.soft_delete.auto._HardDelete; -public class SoftDelete extends _SoftDelete { - - @Override - protected void onPrePersist() { - setDeleted(false); - } +public class HardDelete extends _HardDelete { } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java index 17b70a811..ca11b511a 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/SoftDelete.java @@ -22,9 +22,4 @@ import org.apache.cayenne.testdo.soft_delete.auto._SoftDelete; public class SoftDelete extends _SoftDelete { - @Override - protected void onPrePersist() { - setDeleted(false); - } - } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_HardDelete.java similarity index 69% copy from cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java copy to cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_HardDelete.java index 90761f4ea..52b4f373a 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_HardDelete.java @@ -5,45 +5,32 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import org.apache.cayenne.PersistentObject; -import org.apache.cayenne.exp.property.BaseProperty; import org.apache.cayenne.exp.property.NumericIdProperty; import org.apache.cayenne.exp.property.PropertyFactory; import org.apache.cayenne.exp.property.SelfProperty; import org.apache.cayenne.exp.property.StringProperty; -import org.apache.cayenne.testdo.soft_delete.SoftDelete; +import org.apache.cayenne.testdo.soft_delete.HardDelete; /** - * Class _SoftDelete was generated by Cayenne. + * Class _HardDelete was generated by Cayenne. * It is probably a good idea to avoid changing this class manually, * since it may be overwritten next time code is regenerated. * If you need to make any customizations, please use subclass. */ -public abstract class _SoftDelete extends PersistentObject { +public abstract class _HardDelete extends PersistentObject { private static final long serialVersionUID = 1L; - public static final SelfProperty<SoftDelete> SELF = PropertyFactory.createSelf(SoftDelete.class); + public static final SelfProperty<HardDelete> SELF = PropertyFactory.createSelf(HardDelete.class); - public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "SoftDelete", Integer.class); + public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "HardDelete", Integer.class); public static final String ID_PK_COLUMN = "ID"; - public static final BaseProperty<Boolean> DELETED = PropertyFactory.createBase("deleted", Boolean.class); public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class); - protected Boolean deleted; protected String name; - public void setDeleted(Boolean deleted) { - beforePropertyWrite("deleted", this.deleted, deleted); - this.deleted = deleted; - } - - public Boolean getDeleted() { - beforePropertyRead("deleted"); - return this.deleted; - } - public void setName(String name) { beforePropertyWrite("name", this.name, name); this.name = name; @@ -54,8 +41,6 @@ public abstract class _SoftDelete extends PersistentObject { return this.name; } - protected abstract void onPrePersist(); - @Override public Object readPropertyDirectly(String propName) { if(propName == null) { @@ -63,8 +48,6 @@ public abstract class _SoftDelete extends PersistentObject { } switch(propName) { - case "deleted": - return this.deleted; case "name": return this.name; default: @@ -79,9 +62,6 @@ public abstract class _SoftDelete extends PersistentObject { } switch (propName) { - case "deleted": - this.deleted = (Boolean)val; - break; case "name": this.name = (String)val; break; @@ -101,14 +81,12 @@ public abstract class _SoftDelete extends PersistentObject { @Override protected void writeState(ObjectOutputStream out) throws IOException { super.writeState(out); - out.writeObject(this.deleted); out.writeObject(this.name); } @Override protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { super.readState(in); - this.deleted = (Boolean)in.readObject(); this.name = (String)in.readObject(); } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java index 90761f4ea..9272e2db9 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/soft_delete/auto/_SoftDelete.java @@ -54,8 +54,6 @@ public abstract class _SoftDelete extends PersistentObject { return this.name; } - protected abstract void onPrePersist(); - @Override public Object readPropertyDirectly(String propName) { if(propName == null) { diff --git a/cayenne/src/test/resources/soft-delete.map.xml b/cayenne/src/test/resources/soft-delete.map.xml index 6ded7f6a5..596f417d2 100644 --- a/cayenne/src/test/resources/soft-delete.map.xml +++ b/cayenne/src/test/resources/soft-delete.map.xml @@ -8,11 +8,17 @@ <db-attribute name="DELETED" type="BOOLEAN"/> <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> <db-attribute name="NAME" type="VARCHAR" length="100"/> - <qualifier><![CDATA[DELETED = false]]></qualifier> + <qualifier><![CDATA[DELETED = false or DELETED = null]]></qualifier> + </db-entity> + <db-entity name="HARD_DELETE"> + <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> + <db-attribute name="NAME" type="VARCHAR" length="100"/> </db-entity> <obj-entity name="SoftDelete" className="org.apache.cayenne.testdo.soft_delete.SoftDelete" dbEntityName="SOFT_DELETE"> <obj-attribute name="deleted" type="java.lang.Boolean" db-attribute-path="DELETED"/> <obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/> - <post-add method-name="onPrePersist"/> + </obj-entity> + <obj-entity name="HardDelete" className="org.apache.cayenne.testdo.soft_delete.HardDelete" dbEntityName="HARD_DELETE"> + <obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/> </obj-entity> </data-map>
