This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit cc7959f1406dfe91e7bd91049b537e2cfe0389d7 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Mon Feb 23 15:50:12 2026 -0600 Removed deprecated methods in ClosureEventTriggeringInterceptor --- grails-data-hibernate7/REMOVAL_WARNINGS.md | 27 -- .../support/ClosureEventTriggeringInterceptor.java | 33 +- .../ClosureEventTriggeringInterceptorSpec.groovy | 382 +++++++++++++++++++++ 3 files changed, 401 insertions(+), 41 deletions(-) diff --git a/grails-data-hibernate7/REMOVAL_WARNINGS.md b/grails-data-hibernate7/REMOVAL_WARNINGS.md deleted file mode 100644 index 5af6e42ed2..0000000000 --- a/grails-data-hibernate7/REMOVAL_WARNINGS.md +++ /dev/null @@ -1,27 +0,0 @@ -# Removal Deprecation Warnings — grails-data-hibernate7-core - -Warnings collected by compiling `grails-data-hibernate7-core` with `-Xlint:removal`. -These APIs are **marked for removal** in a future Hibernate / JDK release and must be migrated. - -Generated from: Hibernate `7.1.11.Final` - ---- - -| Fully Qualified Class | Line | Warning | -|---|---|---| -| `org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor` | 286 | `EntityMetamodel in org.hibernate.tuple.entity` has been deprecated and marked for removal | -| `org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor` | 305 | `EntityMetamodel in org.hibernate.tuple.entity` has been deprecated and marked for removal | - ---- - -## Summary by API - -| Deprecated API | Affected Classes | Occurrences | -|---|---|---| -| `LockOptions` (`org.hibernate`) | `GrailsHibernateTemplate` | 3 | -| `Session.get(Class,Object)` / `get(Class,Object,LockOptions)` | `GrailsHibernateTemplate` | 2 | -| `Session.refresh(Object,LockOptions)` | `GrailsHibernateTemplate` | 1 | -| `SchemaAutoTooling` (`org.hibernate.boot`) | `HibernateDatastore` | 4 | -| `IdentifierGenerator.configure(Type,Properties,ServiceRegistry)` | `GrailsIncrementGenerator` | 1 | -| `KeyValue/SimpleValue.createGenerator(Dialect,RootClass)` | `GrailsOneToOne` | 2 | -| `EntityMetamodel` (`org.hibernate.tuple.entity`) | `ClosureEventListener`, `ClosureEventTriggeringInterceptor` | 3 | diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java index 13242871e3..ad107e5722 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java @@ -49,8 +49,9 @@ import org.hibernate.event.spi.PreInsertEvent; import org.hibernate.event.spi.PreLoadEvent; import org.hibernate.event.spi.PreUpdateEvent; import org.hibernate.jpa.event.spi.CallbackRegistry; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.tuple.entity.EntityMetamodel; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -283,30 +284,34 @@ public class ClosureEventTriggeringInterceptor extends AbstractClosureEventTrigg private void updateModifiedPropertiesWithAutoTimestamp( Map<String, Object> modifiedProperties, PreUpdateEvent hibernateEvent) { - EntityMetamodel entityMetamodel = hibernateEvent.getPersister().getEntityMetamodel(); - Integer dateCreatedIdx = - entityMetamodel.getPropertyIndexOrNull(AutoTimestampEventListener.DATE_CREATED_PROPERTY); + + EntityPersister persister = hibernateEvent.getPersister(); + EntityMappingType entityMappingType = persister.getEntityMappingType(); + AttributeMapping dateCreatedMapping = + entityMappingType.findAttributeMapping(AutoTimestampEventListener.DATE_CREATED_PROPERTY); Object[] oldState = hibernateEvent.getOldState(); Object[] state = hibernateEvent.getState(); // Only for "dateCreated" property, "lastUpdated" is handled correctly - if (dateCreatedIdx != null - && oldState != null - && oldState[dateCreatedIdx] != null - && !oldState[dateCreatedIdx].equals(state[dateCreatedIdx])) { - modifiedProperties.put( - AutoTimestampEventListener.DATE_CREATED_PROPERTY, oldState[dateCreatedIdx]); + if (dateCreatedMapping != null) { + int dateCreatedIdx = dateCreatedMapping.getStateArrayPosition(); + if (oldState != null + && oldState[dateCreatedIdx] != null + && !oldState[dateCreatedIdx].equals(state[dateCreatedIdx])) { + modifiedProperties.put( + AutoTimestampEventListener.DATE_CREATED_PROPERTY, oldState[dateCreatedIdx]); + } } } private void synchronizeHibernateState( EntityPersister persister, Object[] state, Map<String, Object> modifiedProperties) { - EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + EntityMappingType entityMappingType = persister.getEntityMappingType(); for (Map.Entry<String, Object> entry : modifiedProperties.entrySet()) { - Integer index = entityMetamodel.getPropertyIndexOrNull(entry.getKey()); - if (index != null) { - state[index] = entry.getValue(); + AttributeMapping attributeMapping = entityMappingType.findAttributeMapping(entry.getKey()); + if (attributeMapping != null) { + state[attributeMapping.getStateArrayPosition()] = entry.getValue(); } } } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy new file mode 100644 index 0000000000..91e11b72f8 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.support + +import grails.gorm.annotation.Entity +import grails.gorm.hibernate.HibernateEntity +import grails.gorm.specs.HibernateGormDatastoreSpec +import grails.gorm.transactions.Rollback +import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent +import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener +import org.grails.datastore.mapping.engine.event.PostDeleteEvent +import org.grails.datastore.mapping.engine.event.PostInsertEvent +import org.grails.datastore.mapping.engine.event.PostLoadEvent +import org.grails.datastore.mapping.engine.event.PostUpdateEvent +import org.grails.datastore.mapping.engine.event.PreDeleteEvent +import org.grails.datastore.mapping.engine.event.PreInsertEvent +import org.grails.datastore.mapping.engine.event.PreLoadEvent +import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.hibernate.engine.spi.SessionFactoryImplementor +import org.hibernate.event.service.spi.EventListenerRegistry +import org.hibernate.event.spi.EventType +import org.springframework.context.ApplicationEvent + +/** + * Integration tests for {@link ClosureEventTriggeringInterceptor}. + * + * The interceptor bridges Hibernate's native event system to GORM's Spring-based + * ApplicationEvent infrastructure. Each test registers a capturing listener on the + * datastore's event publisher so we can assert which GORM events are fired and that + * state mutations made inside a Pre* listener are synchronised back into Hibernate's + * state array (and therefore persisted). + */ +class ClosureEventTriggeringInterceptorSpec extends HibernateGormDatastoreSpec { + + @Override + void setupSpec() { + manager.addAllDomainClasses([ + InterceptorBook, + TimestampedBook, + ]) + } + + // ------------------------------------------------------------------------- + // Helper: add a capturing listener for the duration of one test + // ------------------------------------------------------------------------- + + private CapturingListener addCapturingListener() { + def listener = new CapturingListener(datastore) + ((ConfigurableApplicationEventPublisher) datastore.applicationEventPublisher) + .addApplicationListener(listener) + listener + } + + // ------------------------------------------------------------------------- + // Interceptor is wired into the Hibernate event listener registry + // ------------------------------------------------------------------------- + + void "ClosureEventTriggeringInterceptor is registered for PRE_INSERT in the Hibernate registry"() { + given: + def sfi = sessionFactory.unwrap(SessionFactoryImplementor) + def registry = sfi.serviceRegistry.getService(EventListenerRegistry) + + expect: + registry.getEventListenerGroup(EventType.PRE_INSERT) + .listeners() + .any { it instanceof ClosureEventTriggeringInterceptor } + } + + void "ClosureEventTriggeringInterceptor is registered for PRE_UPDATE, PRE_DELETE, POST_INSERT, POST_UPDATE, POST_DELETE, PRE_LOAD, POST_LOAD"() { + given: + def sfi = sessionFactory.unwrap(SessionFactoryImplementor) + def registry = sfi.serviceRegistry.getService(EventListenerRegistry) + + expect: "all 8 lifecycle event types carry the interceptor" + [ + EventType.PRE_UPDATE, EventType.PRE_DELETE, + EventType.POST_INSERT, EventType.POST_UPDATE, EventType.POST_DELETE, + EventType.PRE_LOAD, EventType.POST_LOAD, + ].every { type -> + registry.getEventListenerGroup(type) + .listeners() + .any { it instanceof ClosureEventTriggeringInterceptor } + } + } + + // ------------------------------------------------------------------------- + // requiresPostCommitHandling + // ------------------------------------------------------------------------- + + void "requiresPostCommitHandling returns false"() { + given: + def sfi = sessionFactory.unwrap(SessionFactoryImplementor) + def registry = sfi.serviceRegistry.getService(EventListenerRegistry) + def interceptor = registry.getEventListenerGroup(EventType.PRE_INSERT) + .listeners() + .find { it instanceof ClosureEventTriggeringInterceptor } as ClosureEventTriggeringInterceptor + + expect: + !interceptor.requiresPostCommitHandling(null) + } + + // ------------------------------------------------------------------------- + // setDatastore – mappingContext wired + // ------------------------------------------------------------------------- + + void "interceptor has a non-null mappingContext after setDatastore"() { + given: + def interceptor = new ClosureEventTriggeringInterceptor() + interceptor.setDatastore(datastore) + + expect: + interceptor.@mappingContext != null + interceptor.@proxyHandler != null + } + + // ------------------------------------------------------------------------- + // Event publishing – each lifecycle publishes the right GORM event type + // ------------------------------------------------------------------------- + + @Rollback + void "saving an entity fires PreInsertEvent then PostInsertEvent"() { + given: + def listener = addCapturingListener() + + when: + new InterceptorBook(title: "Clean Code").save(flush: true, failOnError: true) + + then: + listener.eventTypes.contains(PreInsertEvent) + listener.eventTypes.contains(PostInsertEvent) + listener.eventTypes.indexOf(PreInsertEvent) < listener.eventTypes.indexOf(PostInsertEvent) + } + + @Rollback + void "updating an entity fires PreUpdateEvent then PostUpdateEvent"() { + given: + def book = new InterceptorBook(title: "First").save(flush: true, failOnError: true) + def listener = addCapturingListener() + + when: + book.title = "Second" + book.save(flush: true, failOnError: true) + + then: + listener.eventTypes.contains(PreUpdateEvent) + listener.eventTypes.contains(PostUpdateEvent) + listener.eventTypes.indexOf(PreUpdateEvent) < listener.eventTypes.indexOf(PostUpdateEvent) + } + + @Rollback + void "deleting an entity fires PreDeleteEvent then PostDeleteEvent"() { + given: + def book = new InterceptorBook(title: "Ephemeral").save(flush: true, failOnError: true) + def listener = addCapturingListener() + + when: + book.delete(flush: true) + + then: + listener.eventTypes.contains(PreDeleteEvent) + listener.eventTypes.contains(PostDeleteEvent) + listener.eventTypes.indexOf(PreDeleteEvent) < listener.eventTypes.indexOf(PostDeleteEvent) + } + + @Rollback + void "loading an entity fires PreLoadEvent then PostLoadEvent"() { + given: + def book = new InterceptorBook(title: "Loaded").save(flush: true, failOnError: true) + session.clear() + def listener = addCapturingListener() + + when: + InterceptorBook.get(book.id) + + then: + listener.eventTypes.contains(PreLoadEvent) + listener.eventTypes.contains(PostLoadEvent) + listener.eventTypes.indexOf(PreLoadEvent) < listener.eventTypes.indexOf(PostLoadEvent) + } + + // ------------------------------------------------------------------------- + // State synchronisation – mutations via entityAccess are persisted + // ------------------------------------------------------------------------- + + @Rollback + void "property set via entityAccess in a PreInsertEvent listener is written to the database"() { + given: + ((ConfigurableApplicationEventPublisher) datastore.applicationEventPublisher) + .addApplicationListener(new UpperCaseTitleListener(datastore, PreInsertEvent)) + + when: + def book = new InterceptorBook(title: "lower case").save(flush: true, failOnError: true) + session.clear() + + then: + InterceptorBook.get(book.id).title == "LOWER CASE" + } + + @Rollback + void "property set via entityAccess in a PreUpdateEvent listener is written to the database"() { + given: + def book = new InterceptorBook(title: "original").save(flush: true, failOnError: true) + session.clear() + ((ConfigurableApplicationEventPublisher) datastore.applicationEventPublisher) + .addApplicationListener(new UpperCaseTitleListener(datastore, PreUpdateEvent)) + + when: + def loaded = InterceptorBook.get(book.id) + loaded.title = "updated" + loaded.save(flush: true, failOnError: true) + session.clear() + + then: + InterceptorBook.get(book.id).title == "UPDATED" + } + + // ------------------------------------------------------------------------- + // Dirty checking is activated after PostLoadEvent + // ------------------------------------------------------------------------- + + @Rollback + void "entity loaded from database has dirty checking activated"() { + given: + def book = new InterceptorBook(title: "Track Me").save(flush: true, failOnError: true) + session.clear() + + when: + def loaded = InterceptorBook.get(book.id) + + then: "the loaded entity implements DirtyCheckable and is tracking changes" + loaded instanceof org.grails.datastore.mapping.dirty.checking.DirtyCheckable + ((org.grails.datastore.mapping.dirty.checking.DirtyCheckable) loaded) + .listDirtyPropertyNames() != null + } + + // ------------------------------------------------------------------------- + // Auto-timestamp: dateCreated is preserved on update + // ------------------------------------------------------------------------- + + @Rollback + void "dateCreated is not overwritten when the entity is updated"() { + given: + def book = new TimestampedBook(title: "Original").save(flush: true, failOnError: true) + Date originalDateCreated = book.dateCreated + session.clear() + + when: + def loaded = TimestampedBook.get(book.id) + loaded.title = "Updated" + loaded.save(flush: true, failOnError: true) + session.clear() + + then: + def reloaded = TimestampedBook.get(book.id) + reloaded.dateCreated != null + reloaded.dateCreated == originalDateCreated + } + + // ------------------------------------------------------------------------- + // PreInsertEvent carries a valid entity access + // ------------------------------------------------------------------------- + + @Rollback + void "PreInsertEvent provides a non-null entityAccess for mapped entities"() { + given: + def captured = [] + ((ConfigurableApplicationEventPublisher) datastore.applicationEventPublisher) + .addApplicationListener(new AbstractPersistenceEventListener(datastore) { + @Override + protected void onPersistenceEvent(AbstractPersistenceEvent event) { + if (event instanceof PreInsertEvent && event.entityAccess != null) { + captured << event.entityObject + } + } + @Override + boolean supportsEventType(Class<? extends ApplicationEvent> t) { + t == PreInsertEvent + } + }) + + when: + new InterceptorBook(title: "Access Check").save(flush: true, failOnError: true) + + then: + !captured.isEmpty() + captured[0] instanceof InterceptorBook + } +} + +// --------------------------------------------------------------------------- +// Domain classes +// --------------------------------------------------------------------------- + +@Entity +class InterceptorBook implements HibernateEntity<InterceptorBook> { + String title + + static mapping = { + id generator: 'identity' + } +} + +@Entity +class TimestampedBook implements HibernateEntity<TimestampedBook> { + String title + Date dateCreated + Date lastUpdated + + static mapping = { + id generator: 'identity' + } +} + +// --------------------------------------------------------------------------- +// Helper listeners +// --------------------------------------------------------------------------- + +/** + * Records the Class of every GORM event it receives, in order. + */ +class CapturingListener extends AbstractPersistenceEventListener { + final List<Class<?>> eventTypes = [].asSynchronized() as List<Class<?>> + + CapturingListener(Datastore datastore) { + super(datastore) + } + + @Override + protected void onPersistenceEvent(AbstractPersistenceEvent event) { + eventTypes << event.class + } + + @Override + boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { + AbstractPersistenceEvent.isAssignableFrom(eventType) + } +} + +/** + * Upper-cases the title property via entityAccess in a Pre* event. + */ +class UpperCaseTitleListener extends AbstractPersistenceEventListener { + private final Class<?> targetEventType + + UpperCaseTitleListener(Datastore datastore, Class<?> targetEventType) { + super(datastore) + this.targetEventType = targetEventType + } + + @Override + protected void onPersistenceEvent(AbstractPersistenceEvent event) { + if (event.entityAccess != null) { + String title = event.entityAccess.getProperty("title") as String + if (title) { + event.entityAccess.setProperty("title", title.toUpperCase()) + } + } + } + + @Override + boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { + targetEventType.isAssignableFrom(eventType) + } +}
