You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/10/15 03:04:04 UTC
(grails-core) branch fix/validate-restores-flush-mode-8.0.x created (now 8812b0a2e9)
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a change to branch fix/validate-restores-flush-mode-8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
at 8812b0a2e9 fix: restore session flush mode after a failed validate()
This branch includes the following new commits:
new 8812b0a2e9 fix: restore session flush mode after a failed validate()
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(grails-core) 01/01: fix: restore session flush mode after a failed validate()
Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch fix/validate-restores-flush-mode-8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 8812b0a2e95e1edf6a05c65d106d9cfc231bc284
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Wed Oct 14 20:11:14 2026 -0500
fix: restore session flush mode after a failed validate()
validate() switches the Hibernate session to FlushMode.MANUAL while running
constraints but only restored the previous mode when validation succeeded, so
a failed validate() left the session in MANUAL and the surrounding
transaction never flushed on commit, silently losing unrelated saves.
Restore the previous flush mode unconditionally in finally (Hibernate 5 and 7).
Fixes #14545
Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
.../AbstractHibernateGormValidationApi.groovy | 4 +-
.../ValidateRestoresFlushModeSpec.groovy | 117 +++++++++++++++++++++
.../hibernate/HibernateGormValidationApi.groovy | 4 +-
.../HibernateGormValidationApiSpec.groovy | 99 +++++++++++++++++
4 files changed, 218 insertions(+), 6 deletions(-)
diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy
index bf21ffb7c6..26ec44f293 100644
--- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormValidationApi.groovy
@@ -91,9 +91,7 @@ abstract class AbstractHibernateGormValidationApi<D> extends GormValidationApi<D
validator.validate(instance, errors)
}
} finally {
- if (!errors.hasErrors()) {
- restoreFlushMode(session, previous)
- }
+ restoreFlushMode(session, previous)
}
}
diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/ValidateRestoresFlushModeSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/ValidateRestoresFlushModeSpec.groovy
new file mode 100644
index 0000000000..d9c862ca82
--- /dev/null
+++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/ValidateRestoresFlushModeSpec.groovy
@@ -0,0 +1,117 @@
+/*
+ * 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 grails.gorm.tests.validation
+
+import grails.gorm.annotation.Entity
+import org.grails.orm.hibernate.HibernateDatastore
+import org.hibernate.FlushMode
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+class ValidateRestoresFlushModeSpec extends Specification {
+
+ @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(FlushModeBook)
+
+ void setup() {
+ FlushModeBook.withNewTransaction { FlushModeBook.executeUpdate('delete from FlushModeBook') }
+ }
+
+ void "a failed validate() restores the session flush mode so the transaction still flushes valid saves on commit"() {
+ given:
+ FlushMode modeBefore = null
+ FlushMode modeAfter = null
+ boolean valid = true
+
+ when:
+ FlushModeBook.withTransaction {
+ def session = hibernateDatastore.sessionFactory.currentSession
+ new FlushModeBook(title: 'valid one').save()
+ def invalid = new FlushModeBook(title: null)
+ modeBefore = session.hibernateFlushMode
+ valid = invalid.validate()
+ modeAfter = session.hibernateFlushMode
+ new FlushModeBook(title: 'valid two').save()
+ }
+
+ then:
+ !valid
+ modeAfter == modeBefore
+ modeAfter != FlushMode.MANUAL
+ FlushModeBook.withNewSession { FlushModeBook.list()*.title.sort() } == ['valid one', 'valid two']
+ }
+
+ void "an invalid instance that failed validate() is not persisted by the commit flush"() {
+ when:
+ FlushModeBook.withTransaction {
+ new FlushModeBook(title: 'ok').save()
+ assert !new FlushModeBook(title: null).validate()
+ assert new FlushModeBook(title: null).save() == null
+ }
+
+ then:
+ FlushModeBook.withNewSession { FlushModeBook.list()*.title } == ['ok']
+ }
+
+ void "clearErrors then validate again within a transaction still restores the flush mode"() {
+ given:
+ FlushMode mode = null
+
+ when:
+ FlushModeBook.withTransaction {
+ def session = hibernateDatastore.sessionFactory.currentSession
+ FlushMode before = session.hibernateFlushMode
+ def book = new FlushModeBook(title: null)
+ assert !book.validate()
+ book.clearErrors()
+ assert !book.validate()
+ mode = session.hibernateFlushMode
+ assert mode == before
+ new FlushModeBook(title: 'saved').save()
+ }
+
+ then:
+ mode != FlushMode.MANUAL
+ FlushModeBook.withNewSession { FlushModeBook.list()*.title } == ['saved']
+ }
+
+ void "validate in an earlier transaction does not stop a later transaction from flushing"() {
+ given:
+ def invalid = new FlushModeBook(title: null)
+
+ when:
+ boolean valid = FlushModeBook.withNewTransaction { invalid.validate() }
+ FlushModeBook.withTransaction {
+ new FlushModeBook(title: 'after').save()
+ }
+
+ then:
+ !valid
+ FlushModeBook.withNewSession { FlushModeBook.list()*.title } == ['after']
+ }
+}
+
+@Entity
+class FlushModeBook {
+ String title
+
+ static constraints = {
+ title nullable: false
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
index b34cf0c9b7..14f0775e50 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
@@ -108,9 +108,7 @@ class HibernateGormValidationApi<D> extends GormValidationApi<D> {
validator.validate instance, errors
}
} finally {
- if (!errors.hasErrors()) {
- session.setHibernateFlushMode(previous)
- }
+ session.setHibernateFlushMode(previous)
}
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
index 8c18ad838a..1d33e27f0e 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
@@ -23,6 +23,7 @@ import grails.gorm.transactions.Rollback
import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.core.DatastoreUtils
import org.grails.orm.hibernate.cfg.Settings
+import org.hibernate.FlushMode
import org.springframework.core.env.PropertyResolver
import org.springframework.transaction.PlatformTransactionManager
import spock.lang.AutoCleanup
@@ -105,6 +106,104 @@ class HibernateGormValidationApiSpec extends Specification {
!session.contains(book)
}
+ void "a failed validate() restores the session flush mode so the transaction still flushes valid saves on commit"() {
+ given:
+ ValidatedBook.withNewTransaction { ValidatedBook.executeUpdate('delete from ValidatedBook') }
+ FlushMode modeBefore = null
+ FlushMode modeAfter = null
+ boolean valid = true
+
+ when:
+ ValidatedBook.withTransaction {
+ def session = hibernateDatastore.sessionFactory.currentSession
+ new ValidatedBook(title: 'valid one').save()
+ def invalid = new ValidatedBook(title: null)
+ modeBefore = session.hibernateFlushMode
+ valid = invalid.validate()
+ modeAfter = session.hibernateFlushMode
+ new ValidatedBook(title: 'valid two').save()
+ }
+
+ then:
+ !valid
+ modeAfter == modeBefore
+ modeAfter != FlushMode.MANUAL
+ ValidatedBook.withNewSession { ValidatedBook.list()*.title.sort() } == ['valid one', 'valid two']
+ }
+
+ void "an invalid instance that failed validate() is not persisted by the commit flush when it is never attached"() {
+ given:
+ ValidatedBook.withNewTransaction { ValidatedBook.executeUpdate('delete from ValidatedBook') }
+
+ when:
+ ValidatedBook.withTransaction {
+ new ValidatedBook(title: 'ok').save()
+ assert !new ValidatedBook(title: null).validate()
+ assert new ValidatedBook(title: null).save() == null
+ }
+
+ then:
+ ValidatedBook.withNewSession { ValidatedBook.list()*.title } == ['ok']
+ }
+
+ void "clearErrors then validate again within a transaction still restores the flush mode"() {
+ given:
+ ValidatedBook.withNewTransaction { ValidatedBook.executeUpdate('delete from ValidatedBook') }
+ FlushMode mode = null
+
+ when:
+ ValidatedBook.withTransaction {
+ def session = hibernateDatastore.sessionFactory.currentSession
+ FlushMode before = session.hibernateFlushMode
+ def book = new ValidatedBook(title: null)
+ assert !book.validate()
+ book.clearErrors()
+ assert !book.validate()
+ mode = session.hibernateFlushMode
+ assert mode == before
+ new ValidatedBook(title: 'saved').save()
+ }
+
+ then:
+ mode != FlushMode.MANUAL
+ ValidatedBook.withNewSession { ValidatedBook.list()*.title } == ['saved']
+ }
+
+ void "validate before the transaction begins in an earlier transaction does not stop a later transaction from flushing"() {
+ given:
+ ValidatedBook.withNewTransaction { ValidatedBook.executeUpdate('delete from ValidatedBook') }
+ def invalid = new ValidatedBook(title: null)
+
+ when:
+ boolean valid = ValidatedBook.withNewTransaction { invalid.validate() }
+ ValidatedBook.withTransaction {
+ new ValidatedBook(title: 'after').save()
+ }
+
+ then:
+ !valid
+ ValidatedBook.withNewSession { ValidatedBook.list()*.title } == ['after']
+ }
+
+ void "an invalid dirty instance is not auto flushed by a query after a failed validate()"() {
+ given:
+ ValidatedBook.withNewTransaction { ValidatedBook.executeUpdate('delete from ValidatedBook') }
+ def id = ValidatedBook.withNewTransaction { new ValidatedBook(title: 'original').save(flush: true).id }
+
+ when:
+ ValidatedBook.withTransaction {
+ def book = ValidatedBook.get(id)
+ book.title = null
+ assert !book.validate()
+ ValidatedBook.findAllByTitle('anything')
+ book.discard()
+ }
+
+ then:
+ notThrown(Exception)
+ ValidatedBook.withNewSession { ValidatedBook.get(id).title } == 'original'
+ }
+
@Rollback
void "validate with specific fields only validates those fields"() {
given: