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"/>

Reply via email to