This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-3110
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/ISIS-3110 by this push:
     new 9fd67f45e1 ISIS-3110: reworks JPA and JDO auditing 
(EntityPropertyChange)
9fd67f45e1 is described below

commit 9fd67f45e18fbe01c85395b8a968794d4a41edb0
Author: Dan Haywood <[email protected]>
AuthorDate: Wed Aug 3 19:30:47 2022 +0100

    ISIS-3110: reworks JPA and JDO auditing (EntityPropertyChange)
---
 .../metamodel/objectmanager/ObjectManager.java     |  15 +-
 .../objectlifecycle/ObjectLifecyclePublisher.java  | 152 +++++-----
 .../objectlifecycle/PropertyChangeRecord.java      |  54 ++--
 .../objectlifecycle/PropertyChangeRecordId.java    |  70 +++++
 .../objectlifecycle/PropertyValuePlaceholder.java  |   1 +
 .../IsisModuleCoreRuntimeServices.java             |   2 +
 .../EntityPropertyChangePublisherDefault.java      |  60 ++--
 .../publish/LifecycleCallbackNotifier.java         | 158 ++++++++++
 .../publish/ObjectLifecyclePublisherDefault.java   |  89 +++---
 .../changetracking/EntityChangeTracker.java        |  56 ++--
 .../EntityPropertyChangePublisher.java             |   5 +-
 .../changetracking/EntityChangeTrackerDefault.java | 317 ++++++++-------------
 .../jpa/integration/changetracking/_Xray.java      |  11 -
 .../IsisModulePersistenceJdoDatanucleus.java       |  10 +-
 .../changetracking/JdoLifecycleListener.java       |  39 +--
 .../metamodel/facets/entity/JdoEntityFacet.java    |  11 +-
 .../jpa/applib/integration/IsisEntityListener.java |  65 +++--
 17 files changed, 607 insertions(+), 508 deletions(-)

diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
index 2907699e8d..03204bb5a6 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
@@ -145,7 +145,7 @@ public interface ObjectManager {
     public default ManagedObject adapt(
             final @Nullable Object pojo,
             final @NonNull Supplier<ObjectSpecification> fallbackElementType,
-            final EntityAdaptingMode bookmarking) {
+            final EntityAdaptingMode entityAdaptingMode) {
         if(pojo==null) {
             return ManagedObject.unspecified();
         }
@@ -159,11 +159,11 @@ public interface ObjectManager {
             return ManagedObject.unspecified();
         }
         return spec.isScalar()
-                ? autoBookmarked(spec, pojo, bookmarking)
+                ? managedObjectFor(spec, pojo, entityAdaptingMode)
                 : PackedManagedObject.pack(
                         
spec.getElementSpecification().orElseGet(fallbackElementType),
                         _NullSafe.streamAutodetect(pojo)
-                        .map(element->adapt(element, bookmarking))
+                        .map(element->adapt(element, entityAdaptingMode))
                         .collect(Can.toCan()));
     }
 
@@ -190,7 +190,7 @@ public interface ObjectManager {
                 || 
pojo.getClass().equals(proposedSpec.getCorrespondingClass()))
             // if actual type matches spec's, we assume, that we don't need to 
reload,
             // so this is a shortcut for performance reasons
-            ? autoBookmarked(proposedSpec, pojo, 
EntityAdaptingMode.MEMOIZE_BOOKMARK)
+            ? managedObjectFor(proposedSpec, pojo, 
EntityAdaptingMode.MEMOIZE_BOOKMARK)
             // fallback, ignoring proposedSpec
             : adapt(pojo);
         return adapter;
@@ -198,13 +198,12 @@ public interface ObjectManager {
 
     // -- HELPER
 
-    private static ManagedObject autoBookmarked(
+    private static ManagedObject managedObjectFor(
             final ObjectSpecification spec,
             final Object pojo,
-            final EntityAdaptingMode bookmarking) {
+            final EntityAdaptingMode entityAdaptingMode) {
 
-        if(bookmarking.isMemoize()
-                && spec.isEntity()) {
+        if(entityAdaptingMode.isMemoize() && spec.isEntity()) {
             val entityFacet = spec.getFacet(EntityFacet.class);
             val state = entityFacet.getEntityState(pojo);
             if(state.isAttached()) {
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
index 8d7adce84f..006f1a0b16 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
@@ -20,6 +20,8 @@ package 
org.apache.isis.core.metamodel.services.objectlifecycle;
 
 import java.sql.Timestamp;
 
+import org.springframework.lang.Nullable;
+
 import org.apache.isis.applib.services.factory.FactoryService;
 import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange;
 import org.apache.isis.applib.services.xactn.TransactionId;
@@ -35,109 +37,99 @@ import org.apache.isis.core.metamodel.spec.feature.MixedIn;
 import lombok.NonNull;
 
 /**
- * Responsible for collecting, then immediately publishing changes to domain 
objects,
- * that is,
- * notify publishing subscribers and call the various persistence call-back 
facets.
+ * Responsible for collecting and then passing along changes (to the 
EntityChangeTracker, in persistence commons) so
+ * that they can be published; and is responsible for calling the various 
persistence call-back facets.
  *
  * @since 2.0 {index}
  */
 public interface ObjectLifecyclePublisher {
 
     /**
-     * Independent of the persistence stack, only triggered by {@link 
FactoryService}
-     * and internal {@link ObjectManager}.
+     * Independent of the persistence stack, called when an object has been 
created in-memory, for example by
+     * {@link FactoryService} and internal {@link ObjectManager}.
+     *
+     * <p>
+     *     Default implementation fires off callback/lifecycle events.
+     * </p>
+     *
      * @param domainObject - an entity or view-model
      */
     void onPostCreate(ManagedObject domainObject);
 
+    /**
+     * Called by both JPA and JDO, just after an object is retrieved from the 
database.
+     *
+     * <p>
+     *     Default implementation calls 
<code>EntityChangeTracker#recognizeLoaded(ManagedObject)</code> and
+     *     fires off callback/lifecycle events.
+     * </p>
+     *
+     * @param entity
+     */
     void onPostLoad(ManagedObject entity);
 
+    /**
+     * Called by both JPA and JDO, just before an entity is inserted into the 
database.
+     *
+     * <p>
+     *     Default implementation fires callbacks (including emitting the 
<code>PreStoreEvent</code>, eg as subscribed)
+     *     by the <code>TimestampService</code>.
+     * </p>
+     *
+     * @param entity
+     */
     void onPrePersist(ManagedObject entity);
 
+    /**
+     * Called by both JPA and JDO, just after an entity has been inserted into 
the database.
+     *
+     * <p>
+     *     Default implementation fires callbacks and enlists the entity 
within <code>EntityChangeTracker</code>
+     *     for create/persist.
+     * </p>
+     *
+     * @param entity
+     */
     void onPostPersist(ManagedObject entity);
 
-    void onPreUpdate(ManagedObject entity, Can<PropertyChangeRecord> 
changeRecords);
+    /**
+     * Called by both JPA and JDO (though JDO does <i>not</i> provide any 
changeRecords).
+     *
+     * <p>
+     *     Default implementation fires callbacks and enlists the entity 
within <code>EntityChangeTracker</code>
+     *     for update.
+     * </p>
+     *
+     * @param entity
+     * @param changeRecords - optional parameter to provide the pre-computed 
{@link PropertyChangeRecord}s from the ORM.  JPA does this, JDO does not.
+     */
+    void onPreUpdate(ManagedObject entity, @Nullable Can<PropertyChangeRecord> 
changeRecords);
 
+    /**
+     * Called by both JPA and JDO, after an existing entity has been updated.
+     *
+     * <p>
+     *     Default implementation fires callbacks.
+     * </p>
+     *
+     * @param entity
+     */
     void onPostUpdate(ManagedObject entity);
 
+    /**
+     * Called by both JPA and JDO, just beforean entity is deleted from the 
database.
+     *
+     * <p>
+     *     Default implementation fires callbacks and enlists the entity 
within <code>EntityChangeTracker</code>
+     *     for delete/remove.
+     * </p>
+     *
+     * @param entity
+     */
     void onPreRemove(ManagedObject entity);
 
     //void onPostRemove(ManagedObject entity);
 
-    // -- PUBLISHING PAYLOAD FACTORIES
-
-    static HasEnlistedEntityPropertyChanges publishingPayloadForCreation(
-            final @NonNull ManagedObject entity) {
-
-        return (timestamp, user, txId) -> 
entityPropertyChangesForCreation(timestamp, user, txId, entity);
-    }
-
-    private static Can<EntityPropertyChange> 
entityPropertyChangesForCreation(Timestamp timestamp, String user, 
TransactionId txId, ManagedObject entity) {
-        return propertyChangeRecordsForCreation(entity).stream()
-                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId))
-                .collect(Can.toCan());
-    }
-
-    static Can<PropertyChangeRecord> 
propertyChangeRecordsForCreation(ManagedObject entity) {
-        return entity
-                .getSpecification()
-                .streamProperties(MixedIn.EXCLUDED)
-                .filter(property -> 
EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
-                .filter(property -> 
!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-                .map(property ->
-                        PropertyChangeRecord
-                                .of(
-                                        entity,
-                                        property,
-                                        PreAndPostValue
-                                                
.pre(PropertyValuePlaceholder.NEW)
-                                                
.withPost(ManagedObjects.UnwrapUtil.single(property.get(entity, 
InteractionInitiatedBy.FRAMEWORK)))))
-                .collect(Can.toCan());
-    }
-
-    static HasEnlistedEntityPropertyChanges publishingPayloadForDeletion(
-            final @NonNull ManagedObject entity) {
-
-        return (timestamp, user, txId) -> 
entityPropertyChangesForDeletion(timestamp, user, txId, entity);
-
-    }
-
-    private static Can<EntityPropertyChange> 
entityPropertyChangesForDeletion(Timestamp timestamp, String user, 
TransactionId txId, ManagedObject entity) {
-        return propertyChangeRecordsForDeletion(entity).stream()
-                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId))
-                .collect(Can.toCan());
-    }
-
-    static Can<PropertyChangeRecord> 
propertyChangeRecordsForDeletion(ManagedObject entity) {
-        return entity
-                .getSpecification()
-                .streamProperties(MixedIn.EXCLUDED)
-                .filter(property -> 
EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
-                .filter(property -> 
!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-                .map(property ->
-                        PropertyChangeRecord
-                                .of(
-                                        entity,
-                                        property,
-                                        PreAndPostValue
-                                                
.pre(ManagedObjects.UnwrapUtil.single(property.get(entity, 
InteractionInitiatedBy.FRAMEWORK)))
-                                                
.withPost(PropertyValuePlaceholder.DELETED))
-                )
-                .collect(Can.toCan());
-    }
-
-    static HasEnlistedEntityPropertyChanges publishingPayloadForUpdate(final 
Can<PropertyChangeRecord> changeRecords) {
-        return (timestamp, user, txId) -> 
entityPropertyChangesForUpdate(timestamp, user, txId, changeRecords);
-    }
-
-    private static Can<EntityPropertyChange> 
entityPropertyChangesForUpdate(Timestamp timestamp, String user, TransactionId 
txId, Can<PropertyChangeRecord> changeRecords) {
-        return propertyChangeRecordsForUpdate(changeRecords)
-                .map(pcr -> pcr.toEntityPropertyChange(timestamp, user, txId));
-    }
-
-    static Can<PropertyChangeRecord> 
propertyChangeRecordsForUpdate(Can<PropertyChangeRecord> changeRecords) {
-        return changeRecords;
-    }
 
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java
index 59104cb018..5afa259bb5 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecord.java
@@ -26,7 +26,7 @@ import org.apache.isis.applib.services.xactn.TransactionId;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ManagedObjects;
-import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -34,42 +34,37 @@ import lombok.NonNull;
 import lombok.ToString;
 import lombok.val;
 
-@EqualsAndHashCode(of = {"bookmarkStr", "propertyId"})
-@ToString(of = {"bookmarkStr", "propertyId"})
+@EqualsAndHashCode(of = {"id"})
+@ToString(of = {"id"})
 public final class PropertyChangeRecord {
 
-    @Getter private final ManagedObject entity;
-    @Getter private final ObjectAssociation property;
-    @Getter private final Bookmark bookmark;
-    @Getter private final String propertyId;
+    @Getter
+    private final PropertyChangeRecordId id;
+
+    public ManagedObject getEntity() {return id.getEntity();}
+    public OneToOneAssociation getProperty() {return id.getProperty();}
+    public Bookmark getBookmark() {return id.getBookmark();}
+    public String getPropertyId() {return id.getPropertyId();}
+
     @Getter private PreAndPostValue preAndPostValue;
 
-    private final String bookmarkStr;
 
-    public static PropertyChangeRecord of(
-            final @NonNull ManagedObject entity,
-            final @NonNull ObjectAssociation property) {
-        return new PropertyChangeRecord(entity, property, null);
+    public static @NonNull PropertyChangeRecord of(
+            final @NonNull PropertyChangeRecordId id) {
+        return new PropertyChangeRecord(id, 
PreAndPostValue.pre(PropertyValuePlaceholder.NEW));
     }
 
     public static PropertyChangeRecord of(
-            final @NonNull ManagedObject entity,
-            final @NonNull ObjectAssociation property,
+            final @NonNull PropertyChangeRecordId id,
             final @NonNull PreAndPostValue preAndPostValue) {
-        return new PropertyChangeRecord(entity, property, preAndPostValue);
+        return new PropertyChangeRecord(id, preAndPostValue);
     }
 
     private PropertyChangeRecord(
-            final ManagedObject entity,
-            final ObjectAssociation property,
+            final @NonNull PropertyChangeRecordId id,
             final PreAndPostValue preAndPostValue) {
-        this.entity = entity;
-        this.property = property;
-        this.propertyId = property.getId();
-
-        this.bookmark = ManagedObjects.bookmarkElseFail(entity);
-        this.bookmarkStr = bookmark.toString();
 
+        this.id = id;
         this.preAndPostValue = preAndPostValue;
     }
 
@@ -79,15 +74,15 @@ public final class PropertyChangeRecord {
         return target.getLogicalTypeName() + "#" + propertyId;
     }
 
-    public void setPreValue(final Object pre) {
-        preAndPostValue = PreAndPostValue.pre(pre);
+    public void updatePreValueAsNew() {
+        preAndPostValue = PreAndPostValue.pre(PropertyValuePlaceholder.NEW);
     }
 
-    public void updatePreValue() {
-        setPreValue(getPropertyValue());
+    public void updatePreValueWithCurrent() {
+        preAndPostValue = PreAndPostValue.pre(getPropertyValue());
     }
 
-    public void updatePostValueAsNonDeleted() {
+    public void updatePostValueWithCurrent() {
         preAndPostValue = preAndPostValue.withPost(getPropertyValue());
     }
 
@@ -95,6 +90,7 @@ public final class PropertyChangeRecord {
         preAndPostValue = 
preAndPostValue.withPost(PropertyValuePlaceholder.DELETED);
     }
 
+
     // -- UTILITY
 
     public EntityPropertyChange toEntityPropertyChange(
@@ -120,7 +116,7 @@ public final class PropertyChangeRecord {
     // -- HELPER
 
     private Object getPropertyValue() {
-        val referencedAdapter = property.get(entity, 
InteractionInitiatedBy.FRAMEWORK);
+        val referencedAdapter = getProperty().get(getEntity(), 
InteractionInitiatedBy.FRAMEWORK);
         return ManagedObjects.UnwrapUtil.single(referencedAdapter);
     }
 
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java
new file mode 100644
index 0000000000..57674953be
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyChangeRecordId.java
@@ -0,0 +1,70 @@
+/*
+ *  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
+ *
+ *        http://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.isis.core.metamodel.services.objectlifecycle;
+
+import java.sql.Timestamp;
+
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange;
+import org.apache.isis.applib.services.xactn.TransactionId;
+import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
+import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.ToString;
+import lombok.val;
+
+@EqualsAndHashCode(of = {"bookmarkStr", "propertyId"})
+@ToString(of = {"bookmarkStr", "propertyId"})
+public final class PropertyChangeRecordId {
+
+    @Getter private final String bookmarkStr;
+    @Getter private final String propertyId;
+
+    @Getter private final ManagedObject entity;
+    @Getter private final Bookmark bookmark;
+    @Getter private OneToOneAssociation property;
+
+    public static PropertyChangeRecordId of(
+            final @NonNull ManagedObject entity,
+            final @NonNull OneToOneAssociation property) {
+        return new PropertyChangeRecordId(entity, property);
+    }
+    private PropertyChangeRecordId(
+            final ManagedObject entity,
+            final OneToOneAssociation property) {
+
+        // these exposed as a convenience
+        this.entity = entity;
+        this.property = property;
+        this.bookmark = ManagedObjects.bookmarkElseFail(entity);
+
+        // these are the key
+        this.bookmarkStr = bookmark.toString();
+        this.propertyId = property.getId();
+
+    }
+
+}
+
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java
index 7aebc666d2..7f03f3ecf9 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/objectlifecycle/PropertyValuePlaceholder.java
@@ -25,6 +25,7 @@ package 
org.apache.isis.core.metamodel.services.objectlifecycle;
  */
 public enum PropertyValuePlaceholder {
 
+    UNKNOWN,
     NEW,
     DELETED
     ;
diff --git 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
index b973496825..67f6f84478 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/IsisModuleCoreRuntimeServices.java
@@ -52,6 +52,7 @@ import 
org.apache.isis.core.runtimeservices.publish.CommandPublisherDefault;
 import 
org.apache.isis.core.runtimeservices.publish.EntityChangesPublisherDefault;
 import 
org.apache.isis.core.runtimeservices.publish.EntityPropertyChangePublisherDefault;
 import org.apache.isis.core.runtimeservices.publish.ExecutionPublisherDefault;
+import org.apache.isis.core.runtimeservices.publish.LifecycleCallbackNotifier;
 import 
org.apache.isis.core.runtimeservices.publish.ObjectLifecyclePublisherDefault;
 import 
org.apache.isis.core.runtimeservices.recognizer.ExceptionRecognizerServiceDefault;
 import 
org.apache.isis.core.runtimeservices.recognizer.dae.ExceptionRecognizerForDataAccessException;
@@ -107,6 +108,7 @@ import 
org.apache.isis.core.runtimeservices.xmlsnapshot.XmlSnapshotServiceDefaul
         ObjectIconServiceDefault.class,
         ObjectLifecyclePublisherDefault.class,
         ObjectMementoServiceDefault.class,
+        LifecycleCallbackNotifier.class,
         SchemaValueMarshallerDefault.class,
         ScratchpadDefault.class,
         SerializingAdapterDefault.class,
diff --git 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
index 076ff42315..83e4cda4e2 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
@@ -34,6 +34,7 @@ import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.having.HasEnabling;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges;
 import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
+import org.apache.isis.core.security.util.XrayUtil;
 import 
org.apache.isis.core.transaction.changetracking.EntityPropertyChangePublisher;
 
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -44,6 +45,8 @@ import javax.annotation.PostConstruct;
 import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
+
 import java.util.List;
 
 @Service
@@ -59,6 +62,7 @@ public class EntityPropertyChangePublisherDefault implements 
EntityPropertyChang
     private final ClockService clockService;
     private final TransactionService transactionService;
     private final InteractionLayerTracker iaTracker;
+    private final Provider<HasEnlistedEntityPropertyChanges> 
hasEnlistedEntityPropertyChangesProvider;
 
     private Can<EntityPropertyChangeSubscriber> enabledSubscribers = 
Can.empty();
 
@@ -68,48 +72,50 @@ public class EntityPropertyChangePublisherDefault 
implements EntityPropertyChang
                 .filter(HasEnabling::isEnabled);
     }
 
-    @Override
-    public void publishChangedProperties(
-            final HasEnlistedEntityPropertyChanges 
hasEnlistedEntityPropertyChanges) {
-
-        transactionService.flushTransaction();
-        val payload = getPayload(hasEnlistedEntityPropertyChanges);
-        val xrayHandle = _Xray.enterEntityPropertyChangePublishing(
-                iaTracker,
-                payload,
-                enabledSubscribers,
-                ()->getCannotPublishReason(payload)
-                );
-
-        payload.forEach(propertyChange->{
-            for (val subscriber : enabledSubscribers) {
-                subscriber.onChanging(propertyChange);
-            }
-        });
-
-        _Xray.exitPublishing(xrayHandle);
+    private HasEnlistedEntityPropertyChanges 
getHasEnlistedEntityPropertyChanges() {
+        return hasEnlistedEntityPropertyChangesProvider.get();
     }
 
-    // -- HELPER
+    @Override
+    public void publishChangedProperties() {
 
-    private Can<EntityPropertyChange> getPayload(
-            HasEnlistedEntityPropertyChanges hasEnlistedEntityPropertyChanges) 
{
+        transactionService.flushTransaction();
 
         if(enabledSubscribers.isEmpty()) {
-            return Can.empty();
+            return;
         }
 
         val currentTime = clockService.getClock().nowAsJavaSqlTimestamp();
         val currentUser = userService.currentUserNameElseNobody();
-        val currentTransactionId = transactionService.currentTransactionId()
-                .orElse(TransactionId.empty());
+        val currentTransactionId = 
transactionService.currentTransactionId().orElse(TransactionId.empty());
 
-        return hasEnlistedEntityPropertyChanges.getPropertyChanges(
+        val propertyChanges = 
getHasEnlistedEntityPropertyChanges().getPropertyChanges(
                 currentTime,
                 currentUser,
                 currentTransactionId);
+
+        XrayUtil.SequenceHandle xrayHandle = null;
+        try {
+            xrayHandle = _Xray.enterEntityPropertyChangePublishing(
+                    iaTracker,
+                    propertyChanges,
+                    enabledSubscribers,
+                    () -> getCannotPublishReason(propertyChanges)
+            );
+
+            propertyChanges.forEach(propertyChange->{
+                for (val subscriber : enabledSubscribers) {
+                    subscriber.onChanging(propertyChange);
+                }
+            });
+        } finally {
+            _Xray.exitPublishing(xrayHandle);
+        }
     }
 
+
+    // -- HELPER
+
     // x-ray support
     private @Nullable String getCannotPublishReason(final @NonNull 
Can<EntityPropertyChange> payload) {
         return enabledSubscribers.isEmpty()
diff --git 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java
 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java
new file mode 100644
index 0000000000..d6a941c67e
--- /dev/null
+++ 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/LifecycleCallbackNotifier.java
@@ -0,0 +1,158 @@
+/*
+ *  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
+ *
+ *        http://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.isis.core.runtimeservices.publish;
+
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import org.apache.isis.applib.annotation.InteractionScope;
+import org.apache.isis.applib.annotation.PriorityPrecedence;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.eventbus.EventBusService;
+import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet;
+import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
+import 
org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandlerAbstract;
+import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent;
+import org.apache.isis.core.transaction.changetracking.events.PreStoreEvent;
+
+/**
+ * Calls lifecycle callbacks for entities, ensuring that any given entity is 
only ever called once.
+ * @since 2.0 {@index}
+ */
+@Component
+@Named(IsisModuleCoreRuntimeServices.NAMESPACE + ".LifecycleCallbackNotifier")
+@Priority(PriorityPrecedence.EARLY)
+@Qualifier("Default")
+@InteractionScope
+//@Log4j2
+public class LifecycleCallbackNotifier extends 
PersistenceCallbackHandlerAbstract {
+
+    private final Set<ManagedObject> postCreated = new LinkedHashSet<>();
+    private final Set<ManagedObject> postLoaded = new LinkedHashSet<>();
+    private final Set<ManagedObject> prePersisted = new LinkedHashSet<>();
+    private final Set<ManagedObject> postPersisted = new LinkedHashSet<>();
+    private final Set<ManagedObject> preUpdated = new LinkedHashSet<>();
+    private final Set<ManagedObject> postUpdated = new LinkedHashSet<>();
+    private final Set<ManagedObject> preRemoved = new LinkedHashSet<>();
+
+    @Inject
+    public LifecycleCallbackNotifier(EventBusService eventBusService) {
+        super(eventBusService);
+    }
+
+    public void postCreate(ManagedObject entity) {
+        notify(entity,
+                postCreated,
+                e -> {
+                    CallbackFacet.callCallback(entity, 
CreatedCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
CreatedLifecycleEventFacet.class);
+                });
+    }
+
+    public void postLoad(ManagedObject entity) {
+        notify(entity,
+                postLoaded,
+                e -> {
+                    CallbackFacet.callCallback(entity, 
LoadedCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
LoadedLifecycleEventFacet.class);
+                });
+    }
+
+    public void prePersist(ManagedObject entity) {
+        notify(entity,
+                prePersisted,
+                e -> {
+                    eventBusService.post(PreStoreEvent.of(entity.getPojo()));
+                    CallbackFacet.callCallback(entity, 
PersistingCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
PersistingLifecycleEventFacet.class);
+                });
+    }
+
+    public void postPersist(ManagedObject entity) {
+        notify(entity,
+                postPersisted,
+                e -> {
+                    eventBusService.post(PostStoreEvent.of(entity.getPojo()));
+                    CallbackFacet.callCallback(entity, 
PersistedCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
PersistedLifecycleEventFacet.class);
+                });
+    }
+
+    public void preUpdate(ManagedObject entity) {
+        notify(entity,
+                preUpdated,
+                e -> {
+                    eventBusService.post(PreStoreEvent.of(entity.getPojo()));
+                    CallbackFacet.callCallback(entity, 
UpdatingCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
UpdatingLifecycleEventFacet.class);
+                });
+    }
+
+    public void postUpdate(ManagedObject entity) {
+        notify(entity,
+                postUpdated,
+                e -> {
+                    CallbackFacet.callCallback(entity, 
UpdatedCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
UpdatedLifecycleEventFacet.class);
+                });
+    }
+
+    public void preRemove(ManagedObject entity) {
+        notify(entity,
+                preRemoved,
+                e -> {
+                    CallbackFacet.callCallback(entity, 
RemovingCallbackFacet.class);
+                    postLifecycleEventIfRequired(entity, 
RemovingLifecycleEventFacet.class);
+                });
+    }
+
+    private static void notify(ManagedObject entity, Set<ManagedObject> 
notified, Consumer<ManagedObject> notify) {
+        Optional.of(entity)
+                .filter(x -> !notified.contains(x))
+                .ifPresent(x -> {
+                    notify.accept(entity);
+                    notified.add(x);
+                });
+    }
+
+}
diff --git 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
index cf3aa6f0f6..26feea1bfc 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
@@ -24,33 +24,27 @@ import javax.inject.Named;
 import javax.inject.Provider;
 
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet;
 import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
 import org.apache.isis.core.transaction.changetracking.EntityChangeTracker;
-import 
org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandlerAbstract;
+import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent;
 
 /**
  * @see ObjectLifecyclePublisher
@@ -61,87 +55,68 @@ import 
org.apache.isis.core.transaction.changetracking.PersistenceCallbackHandle
 @Priority(PriorityPrecedence.EARLY)
 @Qualifier("Default")
 //@Log4j2
-public class ObjectLifecyclePublisherDefault
-extends PersistenceCallbackHandlerAbstract
-implements
-    ObjectLifecyclePublisher {
+public class ObjectLifecyclePublisherDefault implements 
ObjectLifecyclePublisher {
 
     private final Provider<EntityChangeTracker> entityChangeTrackerProvider;
+    private final Provider<LifecycleCallbackNotifier> 
lifecycleCallbackNotifierProvider;
 
     @Inject
     public ObjectLifecyclePublisherDefault(
-            final EventBusService eventBusService,
-            final Provider<EntityChangeTracker> entityChangeTrackerProvider) {
-        super(eventBusService);
+            final Provider<EntityChangeTracker> entityChangeTrackerProvider,
+            final Provider<LifecycleCallbackNotifier> 
lifecycleCallbackNotifierProvider) {
         this.entityChangeTrackerProvider = entityChangeTrackerProvider;
+        this.lifecycleCallbackNotifierProvider = 
lifecycleCallbackNotifierProvider;
     }
 
     EntityChangeTracker entityChangeTracker() {
         return entityChangeTrackerProvider.get();
     }
-
-    @Override
-    public void onPostCreate(final ManagedObject domainObject) {
-        CallbackFacet.callCallback(domainObject, CreatedCallbackFacet.class);
-        postLifecycleEventIfRequired(domainObject, 
CreatedLifecycleEventFacet.class);
+    LifecycleCallbackNotifier lifecycleCallbackNotifier() {
+        return lifecycleCallbackNotifierProvider.get();
     }
 
     @Override
-    public void onPrePersist(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, PersistingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
PersistingLifecycleEventFacet.class);
+    public void onPostCreate(final ManagedObject entity) {
+        lifecycleCallbackNotifier().postCreate(entity);
     }
 
     @Override
-    public void onPreUpdate(
-            final ManagedObject entity,
-            final Can<PropertyChangeRecord> changeRecords) {
-
-        if(changeRecords.isEmpty()) {
-            return;
-        }
-
-        CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
UpdatingLifecycleEventFacet.class);
-
-        
if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityChangeTracker().enlistUpdating(entity, changeRecords);
-        }
-
+    public void onPostLoad(final ManagedObject entity) {
+        entityChangeTracker().incrementLoaded(entity);
+        lifecycleCallbackNotifier().postLoad(entity);
     }
 
     @Override
-    public void onPreRemove(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, RemovingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
RemovingLifecycleEventFacet.class);
-
-        
if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityChangeTracker().enlistDeleting(entity, 
ObjectLifecyclePublisher
-                    .propertyChangeRecordsForDeletion(entity));
-        }
+    public void onPrePersist(final ManagedObject entity) {
+        lifecycleCallbackNotifier().prePersist(entity);
     }
 
     @Override
     public void onPostPersist(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, PersistedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
PersistedLifecycleEventFacet.class);
+        entityChangeTracker().enlistCreated(entity);
+        lifecycleCallbackNotifier().postPersist(entity);
+    }
 
-        
if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
-            entityChangeTracker().enlistCreated(entity, 
ObjectLifecyclePublisher
-                    .propertyChangeRecordsForCreation(entity));
-        }
+    @Override
+    public void onPreUpdate(
+            final ManagedObject entity,
+            @Nullable final Can<PropertyChangeRecord> changeRecords) {
+        entityChangeTracker().enlistUpdating(entity, changeRecords);
+        lifecycleCallbackNotifier().preUpdate(entity);
     }
 
+
     @Override
     public void onPostUpdate(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class);
+        lifecycleCallbackNotifier().postUpdate(entity);
     }
 
+
     @Override
-    public void onPostLoad(final ManagedObject entity) {
-        CallbackFacet.callCallback(entity, LoadedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class);
+    public void onPreRemove(final ManagedObject entity) {
+        entityChangeTracker().enlistDeleting(entity);
+        lifecycleCallbackNotifier().preRemove(entity);
     }
 
+
 }
diff --git 
a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
 
b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
index 45acfa6121..eaf09281b4 100644
--- 
a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
+++ 
b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityChangeTracker.java
@@ -18,6 +18,8 @@
  */
 package org.apache.isis.core.transaction.changetracking;
 
+import org.springframework.lang.Nullable;
+
 import org.apache.isis.commons.collections.Can;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
@@ -33,54 +35,50 @@ public interface EntityChangeTracker {
     /**
      * Publishing support: for object stores to enlist an object that has just 
been created,
      * capturing a dummy value <tt>'[NEW]'</tt> for the pre-modification value.
-     * <p>
-     * Fires the appropriate event and lifecycle callback: {@literal PERSISTED}
+     *
      * <p>
      * The post-modification values are captured when the transaction commits.
+     * </p>
      */
     void enlistCreated(ManagedObject entity);
 
-    void enlistCreated(ManagedObject entity, Can<PropertyChangeRecord> 
propertyChangeRecords);
-
-    /**
-     * Publishing support: for object stores to enlist an object that is about 
to be deleted,
-     * capturing the pre-deletion value of the properties of the {@link 
ManagedObject}.
-     * <p>
-     * Fires the appropriate event and lifecycle callback: {@literal REMOVING}
-     * <p>
-     * The post-modification values are captured  when the transaction 
commits.  In the case of deleted objects, a
-     * dummy value <tt>'[DELETED]'</tt> is used as the post-modification value.
-     */
-    void enlistDeleting(ManagedObject entity) ;
-
-    void enlistDeleting(ManagedObject entity, Can<PropertyChangeRecord> 
propertyChangeRecords);
-
     /**
      * Publishing support: for object stores to enlist an object that is about 
to be updated,
      * capturing the pre-modification values of the properties of the {@link 
ManagedObject}.
-     * <p>
-     * Fires the appropriate event and lifecycle callback: {@literal UPDATING}
+     *
      * <p>
      * The post-modification values are captured when the transaction commits.
+     *
+     * <p>
+     * Overload as an optimization for ORMs (specifically, JPA) where already 
have access to the changed records by
+     * accessing the ORM-specific data structures 
(<code>EntityManager</code>'s unit-of-work).
+     *
+     * </p>
+     *
+     * @param entity
+     * @param propertyChangeRecords - optional parameter (as a performance 
optimization) to provide the pre-computed {@link PropertyChangeRecord}s from 
the ORM.  JPA does this, JDO does not.
      */
-    void enlistUpdating(ManagedObject entity);
+    void enlistUpdating(ManagedObject entity, @Nullable 
Can<PropertyChangeRecord> propertyChangeRecords);
 
-    void enlistUpdating(ManagedObject entity, Can<PropertyChangeRecord> 
propertyChangeRecords);
 
     /**
-     * Fires the appropriate event and lifecycle callback: {@literal LOADED}
+     * Publishing support: for object stores to enlist an object that is about 
to be deleted,
+     * capturing the pre-deletion value of the properties of the {@link 
ManagedObject}.
+     *
+     * <p>
+     * The post-modification values are captured  when the transaction 
commits.  In the case of deleted objects, a
+     * dummy value <tt>'[DELETED]'</tt> is used as the post-modification value.
+     * </p>
      */
-    void recognizeLoaded(ManagedObject entity);
+    void enlistDeleting(ManagedObject entity) ;
 
     /**
-     * Fires the appropriate event and lifecycle callback: {@literal 
PERSISTING}
+     * Not strictly part of the concern of entity tracking, but allows the 
default implementation to also implement
+     * the {@link org.apache.isis.applib.services.metrics.MetricsService}.
      */
-    void recognizePersisting(ManagedObject entity);
+    void incrementLoaded(ManagedObject entity);
+
 
-    /**
-     * Fires the appropriate event and lifecycle callback: {@literal UPDATING}
-     */
-    void recognizeUpdating(ManagedObject entity);
 
 
 }
diff --git 
a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java
 
b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java
index 58ff0cff91..7627d74b55 100644
--- 
a/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java
+++ 
b/core/transaction/src/main/java/org/apache/isis/core/transaction/changetracking/EntityPropertyChangePublisher.java
@@ -19,7 +19,6 @@
 package org.apache.isis.core.transaction.changetracking;
 
 import org.apache.isis.applib.services.publishing.spi.EntityPropertyChange;
-import 
org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges;
 
 /**
  * Notifies {@link 
org.apache.isis.applib.services.publishing.spi.EntityPropertyChangeSubscriber}s.
@@ -35,9 +34,7 @@ public interface EntityPropertyChangePublisher {
      * a property of an entity has changed using the
      * {@link 
org.apache.isis.applib.services.publishing.spi.EntityPropertyChangeSubscriber#onChanging(EntityPropertyChange)}
      * callback.
-     *
-     * @param hasEnlistedEntityPropertyChanges
      */
-    void publishChangedProperties(HasEnlistedEntityPropertyChanges 
hasEnlistedEntityPropertyChanges);
+    void publishChangedProperties();
 
 }
diff --git 
a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java
 
b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java
index 91cbbfeaa9..5a429a9f96 100644
--- 
a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java
+++ 
b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/EntityChangeTrackerDefault.java
@@ -19,8 +19,8 @@
  */
 package org.apache.isis.persistence.jpa.integration.changetracking;
 
-import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,28 +51,14 @@ import 
org.apache.isis.applib.services.publishing.spi.EntityPropertyChange;
 import org.apache.isis.applib.services.xactn.TransactionId;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Lazy;
-import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
 import org.apache.isis.commons.internal.collections._Sets;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
-import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet;
-import 
org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet;
 import 
org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
 import 
org.apache.isis.core.metamodel.facets.properties.property.entitychangepublishing.EntityPropertyChangePublishingPolicyFacet;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
-import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyValuePlaceholder;
+import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecordId;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ManagedObjects;
 import org.apache.isis.core.metamodel.spec.feature.MixedIn;
@@ -106,35 +92,15 @@ implements
     HasEnlistedEntityPropertyChanges,
     HasEnlistedEntityChanges {
 
-    /**
-     * If provided by the ORM.
-     */
-    private final List<PropertyChangeRecord> enlistedPropertyChangesOfCreated 
= _Lists.newArrayList();
-    /**
-     * If provided by the ORM.
-     */
-    private final List<PropertyChangeRecord> enlistedPropertyChangesOfUpdated 
= _Lists.newArrayList();
-    /**
-     * If provided by the ORM.
-     */
-    private final List<PropertyChangeRecord> enlistedPropertyChangesOfDeleted 
= _Lists.newArrayList();
 
     /**
-     * Contains initial change records having set the pre-values of every 
property of every object that was enlisted.
-     *
-     * <p>
-     *     ONLY USED IF THE ENLISTED PROPERTY CHANGES ({@link 
#enlistedPropertyChangesOfCreated}, {@link #enlistedPropertyChangesOfUpdated}, 
{@link #enlistedPropertyChangesOfDeleted}) were not provided already.
-     * </p>
+     * Contains a record for every objectId/propertyId that was changed.
      */
-    private final Map<String, PropertyChangeRecord> propertyChangeRecordsById 
= _Maps.newLinkedHashMap();
+    private final Map<PropertyChangeRecordId, PropertyChangeRecord> 
enlistedPropertyChangeRecordsById = _Maps.newLinkedHashMap();
 
     /**
      * Contains pre- and post- values of every property of every object that 
actually changed. A lazy snapshot,
      * triggered by internal call to {@link #snapshotPropertyChangeRecords()}.
-     *
-     * <p>
-     *     ONLY USED IF THE ENLISTED PROPERTY CHANGES ({@link 
#enlistedPropertyChangesOfCreated}, {@link #enlistedPropertyChangesOfUpdated}, 
{@link #enlistedPropertyChangesOfDeleted}) were not provided already.
-     * </p>
      */
     private final _Lazy<Set<PropertyChangeRecord>> 
entityPropertyChangeRecordsForPublishing
         = _Lazy.threadSafe(this::capturePostValuesAndDrain);
@@ -148,6 +114,10 @@ implements
     private final EntityChangesPublisher entityChangesPublisher;
     private final Provider<InteractionProvider> interactionProviderProvider;
 
+    private final LongAdder numberEntitiesLoaded = new LongAdder();
+    private final LongAdder entityChangeEventCount = new LongAdder();
+    private final AtomicBoolean persistentChangesEncountered = new 
AtomicBoolean();
+
     @Inject
     public EntityChangeTrackerDefault(
             final EntityPropertyChangePublisher entityPropertyChangePublisher,
@@ -160,70 +130,22 @@ implements
         this.interactionProviderProvider = interactionProviderProvider;
     }
 
-    private boolean isEnlisted(final @NonNull ManagedObject adapter) {
+    private boolean isEnlistedWrtChangeKind(final @NonNull ManagedObject 
adapter) {
         return ManagedObjects.bookmark(adapter)
         .map(changeKindByEnlistedAdapter::containsKey)
         .orElse(false);
     }
 
-    private void enlistCreatedInternal(final @NonNull ManagedObject adapter, 
@Nullable Can<PropertyChangeRecord> propertyChangeRecords) {
-        if(!isEntityEnabledForChangePublishing(adapter)) {
-            return;
-        }
-        enlistForChangeKindPublishing(adapter, EntityChangeKind.CREATE);
-        if (propertyChangeRecords != null) {
-            // provided by ORM
-            
propertyChangeRecords.forEach(this.enlistedPropertyChangesOfCreated::add);
-        } else {
-            // home-grown approach
-            enlistForPreAndPostValuePublishing(adapter, 
record->record.setPreValue(PropertyValuePlaceholder.NEW));
-        }
-    }
-
-    private void enlistUpdatingInternal(final @NonNull ManagedObject entity, 
Can<PropertyChangeRecord> propertyChangeRecords) {
-        if(!isEntityEnabledForChangePublishing(entity)) {
-            return;
-        }
-        enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE);
-        if(propertyChangeRecords != null) {
-            // provided by ORM
-            
propertyChangeRecords.forEach(this.enlistedPropertyChangesOfUpdated::add);
-        } else {
-            // home-grown approach
-            enlistForPreAndPostValuePublishing(entity, 
PropertyChangeRecord::updatePreValue);
-        }
-    }
-
-    private void enlistDeletingInternal(final @NonNull ManagedObject adapter, 
Can<PropertyChangeRecord> propertyChangeRecords) {
-        if(!isEntityEnabledForChangePublishing(adapter)) {
-            return;
-        }
-        final boolean enlisted = enlistForChangeKindPublishing(adapter, 
EntityChangeKind.DELETE);
-        if(enlisted) {
-            if (propertyChangeRecords != null) {
-                // provided by ORM
-                
propertyChangeRecords.forEach(this.enlistedPropertyChangesOfDeleted::add);
-            } else {
-                // home-grown approach
-                enlistForPreAndPostValuePublishing(adapter, 
PropertyChangeRecord::updatePreValue);
-            }
-        }
-    }
-
     Set<PropertyChangeRecord> snapshotPropertyChangeRecords() {
         // this code path has side-effects, it locks the result for this 
transaction,
         // such that cannot enlist on top of it
         return entityPropertyChangeRecordsForPublishing.get();
     }
 
-    private boolean isOrmSuppliedChangeRecords() {
-        return !(enlistedPropertyChangesOfCreated.isEmpty() && 
enlistedPropertyChangesOfUpdated.isEmpty() && 
enlistedPropertyChangesOfDeleted.isEmpty());
-    }
-
-    private boolean isEntityEnabledForChangePublishing(final @NonNull 
ManagedObject adapter) {
+    private boolean isEntityExcludedForChangePublishing(ManagedObject entity) {
 
-        
if(!EntityChangePublishingFacet.isPublishingEnabled(adapter.getSpecification()))
 {
-            return false; // ignore entities that are not enabled for entity 
change publishing
+        
if(!EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) 
{
+            return true; // ignore entities that are not enabled for entity 
change publishing
         }
 
         if(entityPropertyChangeRecordsForPublishing.isMemoized()) {
@@ -231,10 +153,7 @@ implements
                     + "since changedObjectPropertiesRef was already prepared 
(memoized) for auditing.");
         }
 
-        entityChangeEventCount.increment();
-        enableCommandPublishing();
-
-        return true;
+        return false;
     }
 
     /**
@@ -254,20 +173,14 @@ implements
         _Xray.publish(this, interactionProviderProvider);
 
         log.debug("about to publish entity changes");
-        entityPropertyChangePublisher.publishChangedProperties(this);
+        entityPropertyChangePublisher.publishChangedProperties();
         entityChangesPublisher.publishChangingEntities(this);
     }
 
     private void postPublishing() {
         log.debug("purging entity change records");
 
-        // if ORM provided property change records ... as in JPA
-        this.enlistedPropertyChangesOfCreated.clear();
-        this.enlistedPropertyChangesOfUpdated.clear();
-        this.enlistedPropertyChangesOfDeleted.clear();
-
-        // if instead we had to infer ourselves (home-grown)... as in JDO
-        propertyChangeRecordsById.clear();
+        enlistedPropertyChangeRecordsById.clear();
         entityPropertyChangeRecordsForPublishing.clear();
 
         changeKindByEnlistedAdapter.clear();
@@ -309,12 +222,15 @@ implements
     // -- HELPER
 
     /**
-     * @return <code>true</code> if successfully enlisted, <code>false</code> 
if was already enlisted
+     * @return <code>true</code> if successfully enlisted, <code>false</code> 
if not (no longer) enlisted ... eg delete of an entity that was created earlier 
in the transaction
      */
     private boolean enlistForChangeKindPublishing(
             final @NonNull ManagedObject entity,
             final @NonNull EntityChangeKind changeKind) {
 
+        entityChangeEventCount.increment();
+        enableCommandPublishing();
+
         val bookmark = ManagedObjects.bookmarkElseFail(entity);
 
         val previousChangeKind = changeKindByEnlistedAdapter.get(bookmark);
@@ -345,23 +261,7 @@ implements
         case DELETE:
             return false;
         }
-        return previousChangeKind == null;
-    }
-
-    private void enlistForPreAndPostValuePublishing(
-            final ManagedObject entity,
-            final Consumer<PropertyChangeRecord> onNewChangeRecord) {
-
-        log.debug("enlist entity's property changes for publishing {}", 
entity);
-
-        entity.getSpecification().streamProperties(MixedIn.EXCLUDED)
-        
.filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-        .map(property->PropertyChangeRecord.of(entity, property))
-        
.filter(record->!propertyChangeRecordsById.containsKey(record.getPropertyId())) 
// already enlisted, so ignore
-        .forEach(record->{
-            onNewChangeRecord.accept(record);
-            propertyChangeRecordsById.put(record.getPropertyId(), record);
-        });
+        return false;
     }
 
     /**
@@ -370,34 +270,20 @@ implements
      */
     private Set<PropertyChangeRecord> capturePostValuesAndDrain() {
 
-        Set<PropertyChangeRecord> records;
-
-        if (isOrmSuppliedChangeRecords()) {
-            records = _Sets.newLinkedHashSet();
-            // TODO: might need to make this more sophisticated ?
-            records.addAll(enlistedPropertyChangesOfCreated);
-            records.addAll(enlistedPropertyChangesOfUpdated);
-            records.addAll(enlistedPropertyChangesOfDeleted);
-
-            enlistedPropertyChangesOfCreated.clear();
-            enlistedPropertyChangesOfUpdated.clear();
-            enlistedPropertyChangesOfDeleted.clear();
-        } else {
-            records = propertyChangeRecordsById.values().stream()
-                    // set post values, which have been left empty up to now
-                    .peek(rec->{
-                        // assuming this check correctly detects deleted 
entities (JDO)
-                        
if(ManagedObjects.EntityUtil.isDetachedOrRemoved(rec.getEntity())) {
-                            rec.updatePostValueAsDeleted();
-                        } else {
-                            rec.updatePostValueAsNonDeleted();
-                        }
-                    })
-                    
.filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
-                    .collect(_Sets.toUnmodifiable());
-
-            propertyChangeRecordsById.clear();
-        }
+        val records = enlistedPropertyChangeRecordsById.values().stream()
+                // set post values, which have been left empty up to now
+                .peek(rec -> {
+                    // assuming this check correctly detects deleted entities 
(JDO)
+                    
if(ManagedObjects.EntityUtil.isDetachedOrRemoved(rec.getEntity())) {
+                        rec.updatePostValueAsDeleted();
+                    } else {
+                        rec.updatePostValueWithCurrent();
+                    }
+                })
+                
.filter(managedProperty->managedProperty.getPreAndPostValue().shouldPublish())
+                .collect(_Sets.toUnmodifiable());
+
+        enlistedPropertyChangeRecordsById.clear();
 
         return records;
 
@@ -405,98 +291,119 @@ implements
 
     // side-effect free, used by XRay
     long countPotentialPropertyChangeRecords() {
-        return propertyChangeRecordsById.size();
+        return enlistedPropertyChangeRecordsById.size();
     }
 
-    // -- METRICS SERVICE
+    // -- ENTITY CHANGE TRACKING
 
     @Override
-    public int numberEntitiesLoaded() {
-        return Math.toIntExact(numberEntitiesLoaded.longValue());
+    public void enlistCreated(final ManagedObject entity) {
+
+        _Xray.enlistCreated(entity, interactionProviderProvider);
+
+        if (isEntityExcludedForChangePublishing(entity)) {
+            return;
+        }
+
+        log.debug("enlist entity's property changes for publishing {}", 
entity);
+        enlistForChangeKindPublishing(entity, EntityChangeKind.CREATE);
+
+        enlistForCreateOrUpdate(entity, 
PropertyChangeRecord::updatePreValueAsNew);
     }
 
     @Override
-    public int numberEntitiesDirtied() {
-        return changeKindByEnlistedAdapter.size();
-    }
+    public void enlistUpdating(
+            final ManagedObject entity,
+            @Nullable final Can<PropertyChangeRecord> 
ormPropertyChangeRecords) {
 
-    // -- ENTITY CHANGE TRACKING
+        _Xray.enlistUpdating(entity, interactionProviderProvider);
 
-    @Override
-    public void enlistCreated(final ManagedObject entity) {
-        enlistCreated(entity, null);
-    }
+        if (isEntityExcludedForChangePublishing(entity)) {
+            return;
+        }
 
+        // we call this come what may;
+        // additional properties may now have been changed, and the changeKind 
for publishing might also be modified
+        enlistForChangeKindPublishing(entity, EntityChangeKind.UPDATE);
 
-    @Override
-    public void enlistCreated(ManagedObject entity,  @Nullable final 
Can<PropertyChangeRecord> propertyChangeRecords) {
-        _Xray.enlistCreated(entity, interactionProviderProvider);
-        val hasAlreadyBeenEnlisted = isEnlisted(entity);
-        enlistCreatedInternal(entity, propertyChangeRecords);
+        if(ormPropertyChangeRecords != null) {
+            // provided by ORM
+            ormPropertyChangeRecords
+                    .stream()
+                    .filter(pcr -> 
!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(pcr.getProperty()))
+                    .forEach(pcr -> 
this.enlistedPropertyChangeRecordsById.put(pcr.getId(), pcr)); // if already 
known, then we don't replace (keep first pre-value we know about)
+        } else {
+            // home-grown approach
+            log.debug("enlist entity's property changes for publishing {}", 
entity);
 
-        if(!hasAlreadyBeenEnlisted) {
-            CallbackFacet.callCallback(entity, PersistedCallbackFacet.class);
-            postLifecycleEventIfRequired(entity, 
PersistedLifecycleEventFacet.class);
+            enlistForCreateOrUpdate(entity, 
PropertyChangeRecord::updatePreValueWithCurrent);
         }
     }
 
-    @Override
-    public void enlistDeleting(final ManagedObject entity) {
-        enlistDeleting(entity, null);
+    private void enlistForCreateOrUpdate(ManagedObject entity, 
Consumer<PropertyChangeRecord> propertyChangeRecordConsumer) {
+        entity.getSpecification().streamProperties(MixedIn.EXCLUDED)
+                
.filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
+                .map(property -> PropertyChangeRecordId.of(entity, property))
+                .filter(pcrId -> ! 
enlistedPropertyChangeRecordsById.containsKey(pcrId)) // only if not previously 
seen
+                .map(pcrId -> enlistedPropertyChangeRecordsById.put(pcrId, 
PropertyChangeRecord.of(pcrId)))
+                .filter(Objects::nonNull)   // shouldn't happen, just keeping 
compiler happy
+                .forEach(propertyChangeRecordConsumer);
     }
 
+
     @Override
-    public void enlistDeleting(ManagedObject entity, final 
Can<PropertyChangeRecord> propertyChangeRecords) {
+    public void enlistDeleting(final ManagedObject entity) {
+
         _Xray.enlistDeleting(entity, interactionProviderProvider);
-        enlistDeletingInternal(entity, propertyChangeRecords);
-        CallbackFacet.callCallback(entity, RemovingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
RemovingLifecycleEventFacet.class);
-    }
 
-    @Override
-    public void enlistUpdating(final ManagedObject entity) {
-        enlistUpdating(entity, null);
-    }
+        if (isEntityExcludedForChangePublishing(entity)) {
+            return;
+        }
 
-    @Override
-    public void enlistUpdating(ManagedObject entity, final 
Can<PropertyChangeRecord> propertyChangeRecords) {
-        _Xray.enlistUpdating(entity, interactionProviderProvider);
-        val hasAlreadyBeenEnlisted = isEnlisted(entity);
-        // we call this come what may;
-        // additional properties may now have been changed, and the changeKind 
for publishing might also be modified
-        enlistUpdatingInternal(entity, propertyChangeRecords);
+        final boolean enlisted = enlistForChangeKindPublishing(entity, 
EntityChangeKind.DELETE);
+        if(enlisted) {
 
-        if(!hasAlreadyBeenEnlisted) {
-            // prevent an infinite loop... don't call the 'updating()' 
callback on this object if we have already done so
-            CallbackFacet.callCallback(entity, UpdatingCallbackFacet.class);
-            postLifecycleEventIfRequired(entity, 
UpdatingLifecycleEventFacet.class);
+            log.debug("enlist entity's property changes for publishing {}", 
entity);
+
+            entity.getSpecification()
+                    .streamProperties(MixedIn.EXCLUDED)
+                    .filter(property -> 
EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
+                    .filter(property -> 
!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
+                    .map(property -> PropertyChangeRecordId.of(entity, 
property))
+                    .map(pcrId -> 
enlistedPropertyChangeRecordsById.computeIfAbsent(pcrId, 
PropertyChangeRecord::of))
+                    .forEach(pcr -> {
+                        pcr.updatePreValueWithCurrent();
+                        pcr.updatePostValueAsDeleted();
+                    });
         }
     }
 
+
+
+    /**
+     * Used only for the implementation of {@link MetricsService}.
+     * @param entity
+     */
     @Override
-    public void recognizeLoaded(final ManagedObject entity) {
+    public void incrementLoaded(final ManagedObject entity) {
         _Xray.recognizeLoaded(entity, interactionProviderProvider);
-        CallbackFacet.callCallback(entity, LoadedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, LoadedLifecycleEventFacet.class);
         numberEntitiesLoaded.increment();
     }
 
+
+    // -- METRICS SERVICE
+
     @Override
-    public void recognizePersisting(final ManagedObject entity) {
-        _Xray.recognizePersisting(entity, interactionProviderProvider);
-        CallbackFacet.callCallback(entity, PersistingCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, 
PersistingLifecycleEventFacet.class);
+    public int numberEntitiesLoaded() {
+        return Math.toIntExact(numberEntitiesLoaded.longValue());
     }
 
     @Override
-    public void recognizeUpdating(final ManagedObject entity) {
-        _Xray.recognizeUpdating(entity, interactionProviderProvider);
-        CallbackFacet.callCallback(entity, UpdatedCallbackFacet.class);
-        postLifecycleEventIfRequired(entity, UpdatedLifecycleEventFacet.class);
+    public int numberEntitiesDirtied() {
+        return changeKindByEnlistedAdapter.size();
     }
 
-    private final LongAdder numberEntitiesLoaded = new LongAdder();
-    private final LongAdder entityChangeEventCount = new LongAdder();
-    private final AtomicBoolean persistentChangesEncountered = new 
AtomicBoolean();
+
+
 
 }
diff --git 
a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
 
b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
index 7ee2ef86da..216a435458 100644
--- 
a/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
+++ 
b/persistence/commons/src/main/java/org/apache/isis/persistence/jpa/integration/changetracking/_Xray.java
@@ -91,17 +91,6 @@ final class _Xray {
         addSequence("recognizeLoaded", entity, interactionProviderProvider);
     }
 
-    public static void recognizePersisting(
-            final ManagedObject entity,
-            final Provider<InteractionProvider> interactionProviderProvider) {
-        addSequence("recognizePersisting", entity, 
interactionProviderProvider);
-    }
-
-    public static void recognizeUpdating(
-            final ManagedObject entity,
-            final Provider<InteractionProvider> interactionProviderProvider) {
-        addSequence("recognizeUpdating", entity, interactionProviderProvider);
-    }
 
     // -- HELPER
 
diff --git 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java
 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java
index f3048871c2..e127e956a1 100644
--- 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java
+++ 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/IsisModulePersistenceJdoDatanucleus.java
@@ -48,6 +48,7 @@ import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.core.config.beans.IsisBeanTypeRegistry;
 import 
org.apache.isis.core.config.beans.aoppatch.TransactionInterceptorFactory;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
+import 
org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
 import org.apache.isis.core.transaction.changetracking.EntityChangeTracker;
 import 
org.apache.isis.persistence.jdo.datanucleus.changetracking.JdoLifecycleListener;
 import org.apache.isis.persistence.jdo.datanucleus.config.DatanucleusSettings;
@@ -157,6 +158,7 @@ public class IsisModulePersistenceJdoDatanucleus {
             final DataSource dataSource,
             final MetaModelContext metaModelContext,
             final EventBusService eventBusService,
+            final ObjectLifecyclePublisher objectLifecyclePublisher,
             final Provider<EntityChangeTracker> entityChangeTrackerProvider,
             final IsisBeanTypeRegistry beanTypeRegistry,
             final DatanucleusSettings dnSettings) {
@@ -173,14 +175,14 @@ public class IsisModulePersistenceJdoDatanucleus {
                 val pu = createDefaultPersistenceUnit(beanTypeRegistry);
                 val pmf = new JDOPersistenceManagerFactory(pu, props);
                 pmf.setConnectionFactory(dataSource);
-                integrateWithApplicationLayer(metaModelContext, 
eventBusService, entityChangeTrackerProvider, pmf);
+                integrateWithApplicationLayer(metaModelContext, 
entityChangeTrackerProvider, objectLifecyclePublisher, pmf);
                 return pmf;
             }
             @Override
             protected PersistenceManagerFactory 
newPersistenceManagerFactory(final String name) {
                 val pmf = super.newPersistenceManagerFactory(name);
                 pmf.setConnectionFactory(dataSource); //might be too late, 
anyway, not sure if this is ever called
-                integrateWithApplicationLayer(metaModelContext, 
eventBusService, entityChangeTrackerProvider, pmf);
+                integrateWithApplicationLayer(metaModelContext, 
entityChangeTrackerProvider, objectLifecyclePublisher, pmf);
                 return pmf;
             }
         };
@@ -330,14 +332,14 @@ public class IsisModulePersistenceJdoDatanucleus {
 
     private static void integrateWithApplicationLayer(
             final MetaModelContext metaModelContext,
-            final EventBusService eventBusService,
             final Provider<EntityChangeTracker> entityChangeTrackerProvider,
+            final ObjectLifecyclePublisher objectLifecyclePublisher,
             final PersistenceManagerFactory pmf) {
 
         // install JDO specific entity change listeners ...
 
         val jdoLifecycleListener =
-                new JdoLifecycleListener(metaModelContext, eventBusService, 
entityChangeTrackerProvider);
+                new JdoLifecycleListener(metaModelContext, 
entityChangeTrackerProvider, objectLifecyclePublisher);
         pmf.addInstanceLifecycleListener(jdoLifecycleListener, (Class[]) null);
 
     }
diff --git 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java
 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java
index 647aa388e3..79f182388d 100644
--- 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java
+++ 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/changetracking/JdoLifecycleListener.java
@@ -34,7 +34,9 @@ import org.datanucleus.enhancement.Persistable;
 
 import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
+import 
org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
 import 
org.apache.isis.core.metamodel.objectmanager.ObjectManager.EntityAdaptingMode;
+import 
org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.transaction.changetracking.EntityChangeTracker;
 import org.apache.isis.core.transaction.changetracking.events.PostStoreEvent;
@@ -65,8 +67,8 @@ implements AttachLifecycleListener, ClearLifecycleListener, 
CreateLifecycleListe
 DetachLifecycleListener, DirtyLifecycleListener, LoadLifecycleListener, 
StoreLifecycleListener {
 
     private final @NonNull MetaModelContext metaModelContext;
-    private final @NonNull EventBusService eventBusService;
     private final @NonNull Provider<EntityChangeTracker> 
entityChangeTrackerProvider;
+    private final @NonNull ObjectLifecyclePublisher objectLifecyclePublisher;
 
     // -- CALLBACKS
 
@@ -92,7 +94,9 @@ DetachLifecycleListener, DirtyLifecycleListener, 
LoadLifecycleListener, StoreLif
         log.debug("postLoad {}", ()->_Utils.debug(event));
         final Persistable pojo = _Utils.persistableFor(event);
         val entity = adaptEntityAndInjectServices(pojo, 
EntityAdaptingMode.MEMOIZE_BOOKMARK);
-        getEntityChangeTracker().recognizeLoaded(entity);
+
+        objectLifecyclePublisher.onPostLoad(entity);
+
     }
 
     @Override
@@ -101,13 +105,11 @@ DetachLifecycleListener, DirtyLifecycleListener, 
LoadLifecycleListener, StoreLif
 
         final Persistable pojo = _Utils.persistableFor(event);
 
-        eventBusService.post(PreStoreEvent.of(pojo));
-
         /* Called either when an entity is initially persisted, or when an 
entity is updated; fires the appropriate
          * lifecycle callback. So filter for those events when initially 
persisting. */
         if(pojo.dnGetStateManager().isNew(pojo)) {
             val entity = adaptEntity(pojo, 
EntityAdaptingMode.SKIP_MEMOIZATION);
-            getEntityChangeTracker().recognizePersisting(entity);
+            objectLifecyclePublisher.onPrePersist(entity);
         }
     }
 
@@ -116,23 +118,22 @@ DetachLifecycleListener, DirtyLifecycleListener, 
LoadLifecycleListener, StoreLif
         log.debug("postStore {}", ()->_Utils.debug(event));
 
         final Persistable pojo = _Utils.persistableFor(event);
-
         val entity = adaptEntityAndInjectServices(pojo, 
EntityAdaptingMode.MEMOIZE_BOOKMARK);
 
-        eventBusService.post(PostStoreEvent.of(pojo));
+        
if(EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification())) {
 
-        /* Called either when an entity is initially persisted, or when an 
entity is updated;
-         * fires the appropriate lifecycle callback.*/
-        if(pojo.dnGetStateManager().isNew(pojo)) {
+            /* Called either when an entity is initially persisted, or when an 
entity is updated;
+             * fires the appropriate lifecycle callback.*/
+            if(pojo.dnGetStateManager().isNew(pojo)) {
 
-            getEntityChangeTracker().enlistCreated(entity);
+                objectLifecyclePublisher.onPostPersist(entity);
 
-        } else {
-            // the callback and transaction.enlist are done in the preStore 
callback
-            // (can't be done here, as the enlist requires to capture the 
'before' values)
-            getEntityChangeTracker().recognizeUpdating(entity);
+            } else {
+                // the callback and transaction.enlist are done in the 
preStore callback
+                // (can't be done here, as the enlist requires to capture the 
'before' values)
+                objectLifecyclePublisher.onPostUpdate(entity);
+            }
         }
-
     }
 
 
@@ -142,7 +143,8 @@ DetachLifecycleListener, DirtyLifecycleListener, 
LoadLifecycleListener, StoreLif
 
         final Persistable pojo = _Utils.persistableFor(event);
         val entity = adaptEntity(pojo, EntityAdaptingMode.MEMOIZE_BOOKMARK);
-        getEntityChangeTracker().enlistUpdating(entity);
+
+        objectLifecyclePublisher.onPreUpdate(entity, null);
     }
 
     @Override
@@ -156,7 +158,8 @@ DetachLifecycleListener, DirtyLifecycleListener, 
LoadLifecycleListener, StoreLif
 
         final Persistable pojo = _Utils.persistableFor(event);
         val entity = adaptEntity(pojo, EntityAdaptingMode.SKIP_MEMOIZATION);
-        getEntityChangeTracker().enlistDeleting(entity);
+
+        objectLifecyclePublisher.onPreRemove(entity);
     }
 
     @Override
diff --git 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
index f2ccfe1251..4c92f3a0b1 100644
--- 
a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
+++ 
b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
@@ -38,6 +38,7 @@ import org.apache.isis.applib.query.NamedQuery;
 import org.apache.isis.applib.query.Query;
 import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.bookmark.IdStringifier;
+import 
org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
 import org.apache.isis.core.runtime.idstringifier.IdStringifierLookupService;
 import org.apache.isis.applib.services.exceprecog.Category;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerService;
@@ -412,16 +413,16 @@ implements EntityFacet {
 
     private Can<ManagedObject> fetchWithinTransaction(final Supplier<List<?>> 
fetcher) {
 
-        val entityChangeTracker = 
getFacetHolder().getServiceRegistry().lookupServiceElseFail(EntityChangeTracker.class);
+        val objectLifecyclePublisher = 
getFacetHolder().getServiceRegistry().lookupServiceElseFail(ObjectLifecyclePublisher.class);
 
         return 
getTransactionalProcessor().callWithinCurrentTransactionElseCreateNew(
                 ()->_NullSafe.stream(fetcher.get())
-                    .map(fetchedObject->adopt(entityChangeTracker, 
fetchedObject))
+                    .map(fetchedObject->adopt(objectLifecyclePublisher, 
fetchedObject))
                     .collect(Can.toCan()))
                 .getValue().orElseThrow();
     }
 
-    private ManagedObject adopt(final EntityChangeTracker entityChangeTracker, 
final Object fetchedObject) {
+    private ManagedObject adopt(final ObjectLifecyclePublisher 
objectLifecyclePublisher, final Object fetchedObject) {
         // handles lifecycle callbacks and injects services
 
         // ought not to be necessary, however for some queries it seems that 
the
@@ -429,8 +430,8 @@ implements EntityFacet {
         if(fetchedObject instanceof Persistable) {
             // an entity
             val entity = objectManager.adapt(fetchedObject);
-                    
//fetchResultHandler.initializeEntityAfterFetched((Persistable) fetchedObject);
-            entityChangeTracker.recognizeLoaded(entity);
+
+            objectLifecyclePublisher.onPostLoad(entity);
             return entity;
         } else {
             // a value type
diff --git 
a/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java
 
b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java
index fd70d1a2ff..f49bc6672c 100644
--- 
a/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java
+++ 
b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/integration/IsisEntityListener.java
@@ -31,6 +31,7 @@ import javax.persistence.PreUpdate;
 import org.eclipse.persistence.sessions.UnitOfWork;
 import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
 
+import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.commons.collections.Can;
 import 
org.apache.isis.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
@@ -39,6 +40,8 @@ import 
org.apache.isis.core.metamodel.objectmanager.ObjectManager;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
 import org.apache.isis.core.metamodel.services.objectlifecycle.PreAndPostValue;
 import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
+import 
org.apache.isis.core.metamodel.services.objectlifecycle.PropertyChangeRecordId;
+import org.apache.isis.core.transaction.changetracking.events.PreStoreEvent;
 import org.apache.isis.persistence.jpa.applib.services.JpaSupportService;
 
 import lombok.val;
@@ -67,18 +70,30 @@ public class IsisEntityListener {
     @Inject private ObjectLifecyclePublisher objectLifecyclePublisher;
     @Inject private Provider<JpaSupportService> jpaSupportServiceProvider;
     @Inject private ObjectManager objectManager;
+    @Inject private EventBusService eventBusService;
 
     @PrePersist void onPrePersist(final Object entityPojo) {
         log.debug("onPrePersist: {}", entityPojo);
         serviceInjector.injectServicesInto(entityPojo);
         val entity = objectManager.adapt(entityPojo);
+
         objectLifecyclePublisher.onPrePersist(entity);
     }
 
+    @PostLoad void onPostLoad(final Object entityPojo) {
+        log.debug("onPostLoad: {}", entityPojo);
+        serviceInjector.injectServicesInto(entityPojo);
+        val entity = objectManager.adapt(entityPojo);
+        objectLifecyclePublisher.onPostLoad(entity);
+    }
+
+
     @PreUpdate void onPreUpdate(final Object entityPojo) {
         log.debug("onPreUpdate: {}", entityPojo);
+
         serviceInjector.injectServicesInto(entityPojo);
         val entity = objectManager.adapt(entityPojo);
+
         val entityManagerResult = 
jpaSupportServiceProvider.get().getEntityManager(entityPojo.getClass());
         entityManagerResult.getValue().ifPresent(em -> {  // 
https://wiki.eclipse.org/EclipseLink/FAQ/JPA#How_to_access_what_changed_in_an_object_or_transaction.3F
             val unwrap = em.unwrap(UnitOfWork.class);
@@ -89,32 +104,27 @@ public class IsisEntityListener {
             }
 
             final Can<PropertyChangeRecord> propertyChangeRecords =
-            objectChanges
-            .getChanges()
-            .stream()
-            .filter(property-> 
EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
-            .filter(DirectToFieldChangeRecord.class::isInstance)
-            .map(DirectToFieldChangeRecord.class::cast)
-            .map(changeRecord -> {
-                //XXX lombok val issue with nested lambda
-                final String propertyName = changeRecord.getAttribute();
-                return entity
-                        .getSpecification()
-                        .getProperty(propertyName)
-                        .filter(property->!property.isMixedIn())
-                        
.filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
-                        .map(property->PropertyChangeRecord.of(
-                                entity,
-                                property,
-                                PreAndPostValue
-                                    .pre(changeRecord.getOldValue())
-                                    .withPost(changeRecord.getNewValue())))
-                        .orElse(null); // ignore
-            })
-            .collect(Can.toCan()); // a Can<T> only collects non-null elements
+                objectChanges
+                .getChanges()
+                .stream()
+                .filter(property-> 
EntityChangePublishingFacet.isPublishingEnabled(entity.getSpecification()))
+                .filter(DirectToFieldChangeRecord.class::isInstance)
+                .map(DirectToFieldChangeRecord.class::cast)
+                .map(ormChangeRecord -> {
+                    //XXX lombok val issue with nested lambda
+                    final String propertyName = ormChangeRecord.getAttribute();
+                    return entity
+                            .getSpecification()
+                            .getProperty(propertyName)
+                            .filter(property->!property.isMixedIn())
+                            
.filter(property->!EntityPropertyChangePublishingPolicyFacet.isExcludedFromPublishing(property))
+                            
.map(property->PropertyChangeRecord.of(PropertyChangeRecordId.of(entity, 
property),
+                                                                   
PreAndPostValue.pre(ormChangeRecord.getOldValue())))
+                            .orElse(null); // ignore
+                })
+                .collect(Can.toCan()); // a Can<T> only collects non-null 
elements
 
             objectLifecyclePublisher.onPreUpdate(entity, 
propertyChangeRecords);
-
         });
     }
 
@@ -141,11 +151,4 @@ public class IsisEntityListener {
         log.debug("onPostRemove: {}", entityPojo);
     }
 
-    @PostLoad void onPostLoad(final Object entityPojo) {
-        log.debug("onPostLoad: {}", entityPojo);
-        serviceInjector.injectServicesInto(entityPojo);
-        val entity = objectManager.adapt(entityPojo);
-        objectLifecyclePublisher.onPostLoad(entity);
-    }
-
 }

Reply via email to