This is an automated email from the ASF dual-hosted git repository. aadamchik pushed a commit to branch STABLE-4.1 in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/STABLE-4.1 by this push: new 1eb5e89 CommitLog does not include FKs for deleted objects CAY-2670 1eb5e89 is described below commit 1eb5e8927d0b21819ccb2b4fa2ab5a983af2d508 Author: Andrus Adamchik <and...@objectstyle.com> AuthorDate: Thu Aug 6 14:26:51 2020 +0300 CommitLog does not include FKs for deleted objects CAY-2670 ... reproduced and fixed --- .../cayenne/commitlog/DeletedDiffProcessor.java | 223 +++++++++++---------- .../cayenne/commitlog/CommitLogFilterIT.java | 32 +++ .../cayenne/commitlog/db/AuditableChild1x.java | 9 + .../commitlog/db/auto/_AuditableChild1x.java | 104 ++++++++++ .../commitlog/unit/AuditableServerCase.java | 4 + .../src/test/resources/cayenne-lifecycle.xml | 2 + .../src/test/resources/lifecycle-map.map.xml | 17 +- 7 files changed, 284 insertions(+), 107 deletions(-) diff --git a/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java b/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java index 976ca85..9257281 100644 --- a/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java +++ b/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java @@ -18,123 +18,134 @@ ****************************************************************/ package org.apache.cayenne.commitlog; -import java.util.List; - import org.apache.cayenne.DataChannel; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectId; import org.apache.cayenne.QueryResponse; -import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.commitlog.meta.CommitLogEntity; +import org.apache.cayenne.commitlog.meta.CommitLogEntityFactory; import org.apache.cayenne.commitlog.model.MutableChangeMap; import org.apache.cayenne.commitlog.model.MutableObjectChange; import org.apache.cayenne.commitlog.model.ObjectChangeType; -import org.apache.cayenne.commitlog.meta.CommitLogEntity; -import org.apache.cayenne.commitlog.meta.CommitLogEntityFactory; +import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.query.ObjectIdQuery; -import org.apache.cayenne.reflect.AttributeProperty; -import org.apache.cayenne.reflect.ClassDescriptor; -import org.apache.cayenne.reflect.PropertyVisitor; -import org.apache.cayenne.reflect.ToManyProperty; -import org.apache.cayenne.reflect.ToOneProperty; +import org.apache.cayenne.reflect.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + class DeletedDiffProcessor implements GraphChangeHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(DeletedDiffProcessor.class); - - private CommitLogEntityFactory entityFactory; - private MutableChangeMap changeSet; - private DataChannel channel; - - DeletedDiffProcessor(MutableChangeMap changeSet, DataChannel channel, CommitLogEntityFactory entityFactory) { - this.changeSet = changeSet; - this.channel = channel; - this.entityFactory = entityFactory; - } - - @Override - public void nodeRemoved(Object nodeId) { - ObjectId id = (ObjectId) nodeId; - - final MutableObjectChange objectChangeSet = changeSet.getOrCreate(id, ObjectChangeType.DELETE); - - // TODO: rewrite with SelectById query after Cayenne upgrade - ObjectIdQuery query = new ObjectIdQuery(id, true, ObjectIdQuery.CACHE); - QueryResponse result = channel.onQuery(null, query); - - @SuppressWarnings("unchecked") - List<DataRow> rows = result.firstList(); - - if (rows.isEmpty()) { - LOGGER.warn("No DB snapshot for object to be deleted, no changes will be recorded. ID: " + id); - return; - } - - final DataRow row = rows.get(0); - - ClassDescriptor descriptor = channel.getEntityResolver().getClassDescriptor(id.getEntityName()); - final CommitLogEntity entity = entityFactory.getEntity(id); - - descriptor.visitProperties(new PropertyVisitor() { - - @Override - public boolean visitAttribute(AttributeProperty property) { - - if (!entity.isIncluded(property.getName())) { - return true; - } - - Object value; - if (entity.isConfidential(property.getName())) { - value = Confidential.getInstance(); - } else { - String key = property.getAttribute().getDbAttributeName(); - value = row.get(key); - } - - if (value != null) { - objectChangeSet.attributeChanged(property.getName(), value, null); - } - return true; - } - - @Override - public boolean visitToOne(ToOneProperty property) { - // TODO record FK changes? - return true; - } - - @Override - public boolean visitToMany(ToManyProperty property) { - return true; - } - - }); - } - - @Override - public void nodeIdChanged(Object nodeId, Object newId) { - // do nothing - } - - @Override - public void nodeCreated(Object nodeId) { - // do nothing - } - - @Override - public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { - // do nothing - } - - @Override - public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { - // do nothing - } - - @Override - public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { - // do nothing - } + private static final Logger LOGGER = LoggerFactory.getLogger(DeletedDiffProcessor.class); + + private CommitLogEntityFactory entityFactory; + private MutableChangeMap changeSet; + private DataChannel channel; + + DeletedDiffProcessor(MutableChangeMap changeSet, DataChannel channel, CommitLogEntityFactory entityFactory) { + this.changeSet = changeSet; + this.channel = channel; + this.entityFactory = entityFactory; + } + + @Override + public void nodeRemoved(Object nodeId) { + ObjectId id = (ObjectId) nodeId; + + final MutableObjectChange objectChangeSet = changeSet.getOrCreate(id, ObjectChangeType.DELETE); + + // TODO: rewrite with SelectById query after Cayenne upgrade + ObjectIdQuery query = new ObjectIdQuery(id, true, ObjectIdQuery.CACHE); + QueryResponse result = channel.onQuery(null, query); + + @SuppressWarnings("unchecked") + List<DataRow> rows = result.firstList(); + + if (rows.isEmpty()) { + LOGGER.warn("No DB snapshot for object to be deleted, no changes will be recorded. ID: " + id); + return; + } + + final DataRow row = rows.get(0); + + ClassDescriptor descriptor = channel.getEntityResolver().getClassDescriptor(id.getEntityName()); + final CommitLogEntity entity = entityFactory.getEntity(id); + + descriptor.visitProperties(new PropertyVisitor() { + + @Override + public boolean visitAttribute(AttributeProperty property) { + + if (!entity.isIncluded(property.getName())) { + return true; + } + + Object value; + if (entity.isConfidential(property.getName())) { + value = Confidential.getInstance(); + } else { + String key = property.getAttribute().getDbAttributeName(); + value = row.get(key); + } + + if (value != null) { + objectChangeSet.attributeChanged(property.getName(), value, null); + } + return true; + } + + @Override + public boolean visitToOne(ToOneProperty property) { + if (!entity.isIncluded(property.getName())) { + return true; + } + + // TODO: is there such a thing as "confidential" relationship that we need to hide? + + DbRelationship dbRelationship = property.getRelationship().getDbRelationships().get(0); + + ObjectId value = row.createTargetObjectId( + property.getTargetDescriptor().getEntity().getName(), + dbRelationship); + + if (value != null) { + objectChangeSet.toOneRelationshipDisconnected(property.getName(), value); + } + return true; + } + + @Override + public boolean visitToMany(ToManyProperty property) { + return true; + } + + }); + } + + @Override + public void nodeIdChanged(Object nodeId, Object newId) { + // do nothing + } + + @Override + public void nodeCreated(Object nodeId) { + // do nothing + } + + @Override + public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { + // do nothing + } + + @Override + public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { + // do nothing + } + + @Override + public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { + // do nothing + } } diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java index 78fba95..3a41c07 100644 --- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java +++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java @@ -22,6 +22,7 @@ import org.apache.cayenne.ObjectContext; import org.apache.cayenne.ObjectId; import org.apache.cayenne.commitlog.db.Auditable1; import org.apache.cayenne.commitlog.db.AuditableChild1; +import org.apache.cayenne.commitlog.db.AuditableChild1x; import org.apache.cayenne.commitlog.model.*; import org.apache.cayenne.commitlog.unit.AuditableServerCase; import org.apache.cayenne.configuration.server.ServerRuntimeBuilder; @@ -186,6 +187,37 @@ public class CommitLogFilterIT extends AuditableServerCase { } @Test + public void testPostCommit_Delete_ToOne_OneWay() throws SQLException { + auditable1.insert(1, "xx"); + auditableChild1x.insert(1, 1, "cc1"); + auditableChild1x.insert(2, 1, "cc2"); + + AuditableChild1x ac1 = SelectById.query(AuditableChild1x.class, 2).selectOne(context); + context.deleteObject(ac1); + context.commitChanges(); + + ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class); + verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture()); + + assertNotNull(changeMap.getValue()); + assertEquals(1, changeMap.getValue().getUniqueChanges().size()); + + ObjectChange change = changeMap.getValue().getChanges().get(new ObjectId("AuditableChild1x", AuditableChild1x.ID_PK_COLUMN, 2)); + assertNotNull(change); + assertEquals(ObjectChangeType.DELETE, change.getType()); + + assertEquals(1, change.getAttributeChanges().size()); + assertEquals("cc2", change.getAttributeChanges().get(AuditableChild1x.CHAR_PROPERTY1.getName()).getOldValue()); + assertNull(change.getAttributeChanges().get(AuditableChild1x.CHAR_PROPERTY1.getName()).getNewValue()); + + assertTrue("No 1..N relationships in the entity", change.getToManyRelationshipChanges().isEmpty()); + assertEquals("N..1 state was not captured", 1, change.getToOneRelationshipChanges().size()); + assertEquals(new ObjectId("Auditable1", Auditable1.ID_PK_COLUMN, 1), + change.getToOneRelationshipChanges().get(AuditableChild1x.PARENT.getName()).getOldValue()); + } + + + @Test public void testPostCommit_UpdateToOne() throws SQLException { auditable1.insert(1, "xx"); auditable1.insert(2, "yy"); diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java new file mode 100644 index 0000000..7d8c4a6 --- /dev/null +++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java @@ -0,0 +1,9 @@ +package org.apache.cayenne.commitlog.db; + +import org.apache.cayenne.commitlog.db.auto._AuditableChild1x; + +public class AuditableChild1x extends _AuditableChild1x { + + private static final long serialVersionUID = 1L; + +} diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java new file mode 100644 index 0000000..5203591 --- /dev/null +++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java @@ -0,0 +1,104 @@ +package org.apache.cayenne.commitlog.db.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.BaseDataObject; +import org.apache.cayenne.commitlog.db.Auditable1; +import org.apache.cayenne.exp.Property; + +/** + * Class _AuditableChild1x 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 _AuditableChild1x extends BaseDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "ID"; + + public static final Property<String> CHAR_PROPERTY1 = Property.create("charProperty1", String.class); + public static final Property<Auditable1> PARENT = Property.create("parent", Auditable1.class); + + protected String charProperty1; + + protected Object parent; + + public void setCharProperty1(String charProperty1) { + beforePropertyWrite("charProperty1", this.charProperty1, charProperty1); + this.charProperty1 = charProperty1; + } + + public String getCharProperty1() { + beforePropertyRead("charProperty1"); + return this.charProperty1; + } + + public void setParent(Auditable1 parent) { + setToOneTarget("parent", parent, true); + } + + public Auditable1 getParent() { + return (Auditable1)readProperty("parent"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "charProperty1": + return this.charProperty1; + case "parent": + return this.parent; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "charProperty1": + this.charProperty1 = (String)val; + break; + case "parent": + this.parent = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.charProperty1); + out.writeObject(this.parent); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.charProperty1 = (String)in.readObject(); + this.parent = in.readObject(); + } + +} diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java index 570a54f..67ee30d 100644 --- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java +++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java @@ -34,6 +34,7 @@ public abstract class AuditableServerCase { protected TableHelper auditable1; protected TableHelper auditableChild1; + protected TableHelper auditableChild1x; protected TableHelper auditable2; protected TableHelper auditableChild3; @@ -55,6 +56,8 @@ public abstract class AuditableServerCase { this.auditableChild1 = new TableHelper(dbHelper, "AUDITABLE_CHILD1").setColumns("ID", "AUDITABLE1_ID", "CHAR_PROPERTY1"); + this.auditableChild1x = new TableHelper(dbHelper, "AUDITABLE_CHILD1X").setColumns("ID", "AUDITABLE1_ID", + "CHAR_PROPERTY1"); this.auditable2 = new TableHelper(dbHelper, "AUDITABLE2").setColumns("ID", "CHAR_PROPERTY1", "CHAR_PROPERTY2"); @@ -66,6 +69,7 @@ public abstract class AuditableServerCase { "AUDITABLE3_ID"); this.auditableChild1.deleteAll(); + this.auditableChild1x.deleteAll(); this.auditable1.deleteAll(); this.auditableChild3.deleteAll(); this.auditable2.deleteAll(); diff --git a/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml b/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml index 4144737..0858b03 100644 --- a/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml +++ b/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <domain xmlns="http://cayenne.apache.org/schema/10/domain" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd" project-version="10"> <map name="lifecycle-map"/> <node name="lifecycle-db" diff --git a/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml b/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml index db86a62..2f65c88 100644 --- a/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml +++ b/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd" + xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd" project-version="10"> <property name="defaultPackage" value="org.apache.cayenne.commitlog.db"/> <db-entity name="AUDITABLE1"> @@ -29,6 +29,11 @@ <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/> <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> </db-entity> + <db-entity name="AUDITABLE_CHILD1X"> + <db-attribute name="AUDITABLE1_ID" type="INTEGER"/> + <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/> + <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> + </db-entity> <db-entity name="AUDITABLE_CHILD3"> <db-attribute name="AUDITABLE2_ID" type="INTEGER"/> <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/> @@ -76,6 +81,9 @@ <obj-entity name="AuditableChild1" className="org.apache.cayenne.commitlog.db.AuditableChild1" dbEntityName="AUDITABLE_CHILD1"> <obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/> </obj-entity> + <obj-entity name="AuditableChild1x" className="org.apache.cayenne.commitlog.db.AuditableChild1x" dbEntityName="AUDITABLE_CHILD1X"> + <obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/> + </obj-entity> <obj-entity name="AuditableChild3" className="org.apache.cayenne.commitlog.db.AuditableChild3" dbEntityName="AUDITABLE_CHILD3"> <obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/> <obj-attribute name="charProperty2" type="java.lang.String" db-attribute-path="CHAR_PROPERTY2"/> @@ -87,6 +95,9 @@ <db-relationship name="children1" source="AUDITABLE1" target="AUDITABLE_CHILD1" toMany="true"> <db-attribute-pair source="ID" target="AUDITABLE1_ID"/> </db-relationship> + <db-relationship name="children1x" source="AUDITABLE1" target="AUDITABLE_CHILD1X" toMany="true"> + <db-attribute-pair source="ID" target="AUDITABLE1_ID"/> + </db-relationship> <db-relationship name="children" source="AUDITABLE2" target="AUDITABLE_CHILD3" toMany="true"> <db-attribute-pair source="ID" target="AUDITABLE2_ID"/> </db-relationship> @@ -99,6 +110,9 @@ <db-relationship name="parent" source="AUDITABLE_CHILD1" target="AUDITABLE1"> <db-attribute-pair source="AUDITABLE1_ID" target="ID"/> </db-relationship> + <db-relationship name="parent" source="AUDITABLE_CHILD1X" target="AUDITABLE1"> + <db-attribute-pair source="AUDITABLE1_ID" target="ID"/> + </db-relationship> <db-relationship name="parent" source="AUDITABLE_CHILD3" target="AUDITABLE2"> <db-attribute-pair source="AUDITABLE2_ID" target="ID"/> </db-relationship> @@ -119,6 +133,7 @@ <obj-relationship name="auditable4s" source="Auditable3" target="Auditable4" deleteRule="Deny" db-relationship-path="auditable4s"/> <obj-relationship name="auditable3" source="Auditable4" target="Auditable3" deleteRule="Nullify" db-relationship-path="auditable3"/> <obj-relationship name="parent" source="AuditableChild1" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/> + <obj-relationship name="parent" source="AuditableChild1x" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/> <obj-relationship name="parent" source="AuditableChild3" target="Auditable2" deleteRule="Nullify" db-relationship-path="parent"/> <obj-relationship name="e4s" source="E3" target="E4" deleteRule="Deny" db-relationship-path="e34s.e4"/> <obj-relationship name="e3s" source="E4" target="E3" deleteRule="Deny" db-relationship-path="e34s.e3"/>