From bc1808ab594ca196143575d96c957637fea0bfde Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Wed, 8 Oct 2025 11:33:34 -0700 Subject: [PATCH] Mark @AutoTimestamp properties dirty so they are persisted across all datastores. Fixes #15120 --- .../codecs/PersistentEntityCodec.groovy | 7 --- .../events/AutoTimestampEventListener.java | 45 +++++++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy index bb0303fd247..24fbd6a9bcd 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy @@ -64,7 +64,6 @@ import org.grails.datastore.mapping.engine.internal.MappingUtils import org.grails.datastore.mapping.model.EmbeddedPersistentEntity import org.grails.datastore.mapping.model.PersistentEntity import org.grails.datastore.mapping.model.PersistentProperty -import org.grails.datastore.mapping.model.config.GormProperties import org.grails.datastore.mapping.model.types.Association import org.grails.datastore.mapping.model.types.Embedded import org.grails.datastore.mapping.model.types.EmbeddedCollection @@ -249,12 +248,6 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { } } - else { - // schedule lastUpdated if necessary - if (entity.getPropertyByName(GormProperties.LAST_UPDATED) != null) { - dirtyProperties.add(GormProperties.LAST_UPDATED) - } - } for (propertyName in dirtyProperties) { def prop = entity.getPropertyByName(propertyName) diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java index 8da88a0b34e..d3c82216e32 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java @@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEvent; +import org.springframework.util.ReflectionUtils; import grails.gorm.annotation.AutoTimestamp; import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider; @@ -39,6 +40,7 @@ import org.grails.datastore.mapping.config.Entity; import org.grails.datastore.mapping.config.Settings; import org.grails.datastore.mapping.core.Datastore; +import org.grails.datastore.mapping.dirty.checking.DirtyCheckable; import org.grails.datastore.mapping.engine.EntityAccess; import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent; import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener; @@ -148,10 +150,25 @@ private void initializeIfNecessary(PersistentEntity entity, String name) { public boolean beforeUpdate(PersistentEntity entity, EntityAccess ea) { Set props = getLastUpdatedPropertyNames(entity.getName()); if (props != null) { + Object entityObject = ea.getEntity(); + boolean isDirtyCheckable = entityObject instanceof DirtyCheckable; + + // For dirty-checking datastores (e.g., MongoDB), only set autotimestamp if entity has dirty properties + if (isDirtyCheckable) { + List dirtyPropertyNames = ((DirtyCheckable) entityObject).listDirtyPropertyNames(); + if (dirtyPropertyNames.isEmpty()) { + return true; + } + } + for (String prop : props) { Class lastUpdateType = ea.getPropertyType(prop); Object timestamp = timestampProvider.createTimestamp(lastUpdateType); ea.setProperty(prop, timestamp); + // Mark property as dirty for datastores that use dirty checking (e.g., MongoDB) + if (isDirtyCheckable) { + ((DirtyCheckable) entityObject).markDirty(prop); + } } } return true; @@ -167,18 +184,6 @@ protected Set getDateCreatedPropertyNames(String entityName) { return properties == null ? null : properties.orElse(null); } - private static Field getFieldFromHierarchy(Class entity, String fieldName) { - Class clazz = entity; - while (clazz != null) { - try { - return clazz.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - clazz = clazz.getSuperclass(); - } - } - return null; - } - protected void storeDateCreatedAndLastUpdatedInfo(PersistentEntity persistentEntity) { if (persistentEntity.isInitialized()) { ClassMapping classMapping = persistentEntity.getMapping(); @@ -190,13 +195,15 @@ protected void storeDateCreatedAndLastUpdatedInfo(PersistentEntity persistentEnt } else if (property.getName().equals(DATE_CREATED_PROPERTY)) { storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property); } else { - Field field = getFieldFromHierarchy(persistentEntity.getJavaClass(), property.getName()); - if (field != null && field.isAnnotationPresent(AutoTimestamp.class)) { - AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class); - if (autoTimestamp.value() == AutoTimestamp.EventType.UPDATED) { - storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property); - } else { - storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property); + Field field = ReflectionUtils.findField(persistentEntity.getJavaClass(), property.getName()); + if (field != null) { + if (field.isAnnotationPresent(AutoTimestamp.class)) { + AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class); + if (autoTimestamp.value() == AutoTimestamp.EventType.UPDATED) { + storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property); + } else { + storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property); + } } } }