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 1a48917e03d8c00c7b47239f31e518fdc7cdda8b Author: Walter Duque de Estrada <[email protected]> AuthorDate: Mon Feb 23 15:16:17 2026 -0600 Removed deprecated methods in ClosureEventListener --- grails-data-hibernate7/REMOVAL_WARNINGS.md | 1 - .../grails/orm/hibernate/HibernateDatastore.java | 4 +- .../hibernate/support/ClosureEventListener.java | 10 +- .../support/ClosureEventListenerSpec.groovy | 359 +++++++++++++++++++++ 4 files changed, 367 insertions(+), 7 deletions(-) diff --git a/grails-data-hibernate7/REMOVAL_WARNINGS.md b/grails-data-hibernate7/REMOVAL_WARNINGS.md index 347ade680f..5af6e42ed2 100644 --- a/grails-data-hibernate7/REMOVAL_WARNINGS.md +++ b/grails-data-hibernate7/REMOVAL_WARNINGS.md @@ -9,7 +9,6 @@ Generated from: Hibernate `7.1.11.Final` | Fully Qualified Class | Line | Warning | |---|---|---| -| `org.grails.orm.hibernate.support.ClosureEventListener` | 342 | `EntityMetamodel in org.hibernate.tuple.entity` has been deprecated and marked for removal | | `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 | diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java index 2b784df2be..3a739069c5 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java @@ -717,14 +717,14 @@ public class HibernateDatastore extends AbstractHibernateDatastore implements Me dataSource = new MultiTenantDataSource(dataSource, schemaName) { @Override - public Connection getConnection() { + public Connection getConnection() throws SQLException { Connection connection = super.getConnection(); schemaHandler.useSchema(connection, schemaName); return new MultiTenantConnection(connection, schemaHandler); } @Override - public Connection getConnection(String username, String password){ + public Connection getConnection(String username, String password) throws SQLException { Connection connection = super.getConnection(username, password); schemaHandler.useSchema(connection, schemaName); return new MultiTenantConnection(connection, schemaHandler); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java index 3eaf7d9949..870fd38fc7 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java @@ -46,8 +46,9 @@ import org.hibernate.engine.spi.ExecutableList; import org.hibernate.event.spi.*; import org.hibernate.jpa.event.spi.CallbackRegistry; import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; +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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.ReflectionUtils; @@ -339,11 +340,12 @@ public class ClosureEventListener Object entity = event.getEntity(); EntityReflector reflector = persistentEntity.getReflector(); HashMap<Integer, Object> changedState = new HashMap<>(); - EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + EntityMappingType entityMappingType = persister.getEntityMappingType(); for (int i = 0; i < propertyNames.length; i++) { String p = propertyNames[i]; - Integer index = entityMetamodel.getPropertyIndexOrNull(p); - if (index == null) continue; + AttributeMapping attributeMapping = entityMappingType.findAttributeMapping(p); + if (attributeMapping == null) continue; + int index = attributeMapping.getStateArrayPosition(); PersistentProperty property = persistentEntity.getPropertyByName(p); if (property == null) { diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy new file mode 100644 index 0000000000..32c3ca50e0 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy @@ -0,0 +1,359 @@ +/* + * 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 grails.validation.ValidationException +import org.springframework.orm.hibernate5.HibernateSystemException + +class ClosureEventListenerSpec extends HibernateGormDatastoreSpec { + + @Override + void setupSpec() { + manager.addAllDomainClasses([ + EventBook, + ValidatedBook, + MutatingBook, + ]) + } + + void cleanup() { + EventBook.callLog.clear() + } + + // ------------------------------------------------------------------------- + // beforeInsert + // ------------------------------------------------------------------------- + + @Rollback + void "beforeInsert is called before a new entity is persisted"() { + when: + new EventBook(title: "Groovy in Action").save(flush: true, failOnError: true) + + then: + 'beforeInsert' in EventBook.callLog + } + + @Rollback + void "beforeInsert returning false vetoes the insert"() { + given: + EventBook.vetoInsert = true + + when: + new EventBook(title: "Vetoed Book").save(flush: true) + + then: + thrown(HibernateSystemException) + + cleanup: + EventBook.vetoInsert = false + } + + // ------------------------------------------------------------------------- + // afterInsert + // ------------------------------------------------------------------------- + + @Rollback + void "afterInsert is called after a new entity is persisted"() { + when: + new EventBook(title: "Clean Code").save(flush: true, failOnError: true) + + then: + 'afterInsert' in EventBook.callLog + } + + // ------------------------------------------------------------------------- + // beforeUpdate / afterUpdate + // ------------------------------------------------------------------------- + + @Rollback + void "beforeUpdate is called before an existing entity is updated"() { + given: + def book = new EventBook(title: "Original Title").save(flush: true, failOnError: true) + EventBook.callLog.clear() + + when: + book.title = "Updated Title" + book.save(flush: true, failOnError: true) + + then: + 'beforeUpdate' in EventBook.callLog + } + + @Rollback + void "afterUpdate is called after an existing entity is updated"() { + given: + def book = new EventBook(title: "First Edition").save(flush: true, failOnError: true) + EventBook.callLog.clear() + + when: + book.title = "Second Edition" + book.save(flush: true, failOnError: true) + + then: + 'afterUpdate' in EventBook.callLog + } + + // ------------------------------------------------------------------------- + // beforeDelete / afterDelete + // ------------------------------------------------------------------------- + + @Rollback + void "beforeDelete is called before an entity is deleted"() { + given: + def book = new EventBook(title: "Ephemeral Book").save(flush: true, failOnError: true) + EventBook.callLog.clear() + + when: + book.delete(flush: true) + + then: + 'beforeDelete' in EventBook.callLog + } + + @Rollback + void "afterDelete is called after an entity is deleted"() { + given: + def book = new EventBook(title: "Gone Book").save(flush: true, failOnError: true) + EventBook.callLog.clear() + + when: + book.delete(flush: true) + + then: + 'afterDelete' in EventBook.callLog + } + + @Rollback + void "beforeDelete returning false vetoes the delete"() { + given: + def book = new EventBook(title: "Protected Book").save(flush: true, failOnError: true) + Long id = book.id + EventBook.vetoDelete = true + + when: + book.delete(flush: true) + + then: + EventBook.get(id) != null + + cleanup: + EventBook.vetoDelete = false + } + + // ------------------------------------------------------------------------- + // onLoad / afterLoad + // ------------------------------------------------------------------------- + + @Rollback + void "onLoad is called when an entity is loaded from the database"() { + given: + def book = new EventBook(title: "Loaded Book").save(flush: true, failOnError: true) + session.clear() + EventBook.callLog.clear() + + when: + EventBook.get(book.id) + + then: + 'onLoad' in EventBook.callLog + } + + @Rollback + void "afterLoad is called after an entity is loaded from the database"() { + given: + def book = new EventBook(title: "After Load Book").save(flush: true, failOnError: true) + session.clear() + EventBook.callLog.clear() + + when: + EventBook.get(book.id) + + then: + 'afterLoad' in EventBook.callLog + } + + // ------------------------------------------------------------------------- + // beforeValidate + // ------------------------------------------------------------------------- + + @Rollback + void "beforeValidate is called before validation runs"() { + when: + new EventBook(title: "Validated").save(flush: true, failOnError: true) + + then: + 'beforeValidate' in EventBook.callLog + } + + // ------------------------------------------------------------------------- + // failOnError — validation failure throws ValidationException + // ------------------------------------------------------------------------- + + void "validation failure with failOnError throws ValidationException"() { + when: + ValidatedBook.withTransaction { + new ValidatedBook(title: null).save(flush: true, failOnError: true) + } + + then: + thrown(ValidationException) + } + + void "validation failure without failOnError returns null"() { + when: + def book = ValidatedBook.withTransaction { + new ValidatedBook(title: null).save(flush: true) + } + + then: + book == null || book.hasErrors() + } + + // ------------------------------------------------------------------------- + // beforeInsert can mutate state that gets persisted + // ------------------------------------------------------------------------- + + @Rollback + void "property mutation in beforeInsert is reflected in the persisted state"() { + when: + def book = new MutatingBook(title: "raw title").save(flush: true, failOnError: true) + session.clear() + def reloaded = MutatingBook.get(book.id) + + then: + reloaded.title == "RAW TITLE" + } + + @Rollback + void "property mutation in beforeUpdate is reflected in the persisted state"() { + given: + def book = new MutatingBook(title: "first").save(flush: true, failOnError: true) + session.clear() + + when: + def loaded = MutatingBook.get(book.id) + loaded.title = "second" + loaded.save(flush: true, failOnError: true) + session.clear() + def reloaded = MutatingBook.get(book.id) + + then: + reloaded.title == "SECOND" + } +} + +// --------------------------------------------------------------------------- +// Domain class with all event hooks instrumented +// --------------------------------------------------------------------------- + +@Entity +class EventBook implements HibernateEntity<EventBook> { + + String title + + static callLog = [].asSynchronized() as List<String> + static boolean vetoInsert = false + static boolean vetoDelete = false + + static mapping = { + id generator: 'identity' + } + + def beforeInsert() { + callLog << 'beforeInsert' + return vetoInsert ? false : null + } + + def afterInsert() { + callLog << 'afterInsert' + } + + def beforeUpdate() { + callLog << 'beforeUpdate' + } + + def afterUpdate() { + callLog << 'afterUpdate' + } + + def beforeDelete() { + callLog << 'beforeDelete' + return vetoDelete ? false : null + } + + def afterDelete() { + callLog << 'afterDelete' + } + + def onLoad() { + callLog << 'onLoad' + } + + def afterLoad() { + callLog << 'afterLoad' + } + + def beforeValidate() { + callLog << 'beforeValidate' + } +} + +// --------------------------------------------------------------------------- +// Domain class for validation tests +// --------------------------------------------------------------------------- + +@Entity +class ValidatedBook implements HibernateEntity<ValidatedBook> { + + String title + + static mapping = { + id generator: 'identity' + } + + static constraints = { + title nullable: false, blank: false + } +} + +// --------------------------------------------------------------------------- +// Domain class that mutates state in event hooks +// --------------------------------------------------------------------------- + +@Entity +class MutatingBook implements HibernateEntity<MutatingBook> { + + String title + + static mapping = { + id generator: 'identity' + } + + def beforeInsert() { + title = title?.toUpperCase() + } + + def beforeUpdate() { + title = title?.toUpperCase() + } +}
