Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 16ccfd4

Browse files
committed
fix #1155 - skipValidate logic in GormValidateable causes missed validations and is inconsistent
1 parent ee37c0a commit 16ccfd4

File tree

2 files changed

+112
-9
lines changed

2 files changed

+112
-9
lines changed

grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import org.grails.datastore.mapping.core.Datastore
2323
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
2424
import org.grails.datastore.mapping.model.config.GormProperties
2525
import org.grails.datastore.mapping.model.types.Embedded
26-
import org.grails.datastore.mapping.model.types.EmbeddedCollection
27-
import org.grails.datastore.mapping.model.types.ToMany
2826
import org.grails.datastore.mapping.proxy.ProxyHandler
2927
import org.grails.datastore.mapping.reflect.ClassUtils
3028
import org.grails.datastore.mapping.reflect.EntityReflector
@@ -143,12 +141,12 @@ abstract class AbstractHibernateGormInstanceApi<D> extends GormInstanceApi<D> {
143141
// this piece of code will retrieve a persistent instant
144142
// of a domain class property is only the id is set thus
145143
// relieving this burden off the developer
146-
autoRetrieveAssocations datastore, domainClass, target
144+
autoRetrieveAssociations datastore, domainClass, target
147145

148-
// If validation is disabled, skip it or if a flush:true is passed then disable it too to avoid duplicate validation
146+
// Once we get here we've either validated this object or skipped validation, either way
147+
// we don't need to validate again for the rest of this save.
149148
GormValidateable validateable = (GormValidateable) target
150-
boolean shouldSkipValidation = !shouldValidate || shouldFlush
151-
validateable.skipValidation(shouldSkipValidation)
149+
validateable.skipValidation(true)
152150

153151
try {
154152
if (shouldInsert(arguments)) {
@@ -164,7 +162,10 @@ abstract class AbstractHibernateGormInstanceApi<D> extends GormInstanceApi<D> {
164162
return performSave(target, shouldFlush)
165163
}
166164
} finally {
167-
validateable.skipValidation(!shouldFlush)
165+
// After save, we have to make sure this entity is setup to validate again. It's possible it will
166+
// be validated again if this save didn't flush, but without checking it's dirty state we can't really
167+
// know for sure that it hasn't changed and need to err on the side of caution.
168+
validateable.skipValidation(false)
168169
}
169170
}
170171

@@ -284,7 +285,7 @@ abstract class AbstractHibernateGormInstanceApi<D> extends GormInstanceApi<D> {
284285
try {
285286
session.flush()
286287
} catch (HibernateException e) {
287-
// session should not be flushed again after a data acccess exception!
288+
// session should not be flushed again after a data access exception!
288289
session.setFlushMode FlushMode.MANUAL
289290
throw e
290291
}
@@ -295,7 +296,7 @@ abstract class AbstractHibernateGormInstanceApi<D> extends GormInstanceApi<D> {
295296
* @param target The target object
296297
*/
297298
@SuppressWarnings("unchecked")
298-
private void autoRetrieveAssocations(Datastore datastore, PersistentEntity entity, Object target) {
299+
private void autoRetrieveAssociations(Datastore datastore, PersistentEntity entity, Object target) {
299300
EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity)
300301
IHibernateTemplate t = this.hibernateTemplate
301302
for (PersistentProperty prop in entity.associations) {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package grails.gorm.tests.validation
2+
3+
import grails.gorm.annotation.Entity
4+
import grails.gorm.transactions.Rollback
5+
import grails.validation.ValidationException
6+
import org.grails.orm.hibernate.HibernateDatastore
7+
import spock.lang.AutoCleanup
8+
import spock.lang.Shared
9+
import spock.lang.Specification
10+
11+
class SkipValidationSpec extends Specification {
12+
@Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore(Author)
13+
14+
// For whatever reason it may be valid to flush & save without validation (database would obviously fail if the field is too long, but maybe the object is expected to only have an invalid validator?) so continue to support this scenario
15+
@Rollback
16+
void "calling save with flush with validate false should skip validation"() {
17+
when:
18+
new Author(name: 'false').save(failOnError: true, validate: false, flush: true)
19+
20+
then:
21+
noExceptionThrown()
22+
}
23+
24+
@Rollback
25+
void "calling save with flush and invalid attribute"() {
26+
when:
27+
new Author(name: 'ThisNameIsTooLong').save(failOnError: true, flush: true)
28+
29+
then:
30+
thrown(ValidationException)
31+
}
32+
33+
@Rollback
34+
void "calling validate with property list after save should validate again"() {
35+
// Save but don't flush, this causes the new author to have skipValidate = true
36+
Author author = new Author(name: 'Aaron').save(failOnError: true)
37+
38+
when: "validate is called again with a property list"
39+
author.name = "ThisNameIsTooLong"
40+
def isValid = author.validate(['name'])
41+
42+
then: "it should be invalid but it skips validation instead"
43+
!isValid
44+
}
45+
46+
@Rollback
47+
void "calling validate with property list after save with flush should validate again"() {
48+
// Save but don't flush, this causes the new author to have skipValidate = true
49+
Author author = new Author(name: 'Aaron').save(failOnError: true, flush: true)
50+
51+
when: "validate is called again with a property list"
52+
author.name = "ThisNameIsTooLong"
53+
def isValid = author.validate(['name'])
54+
55+
then: "it should be invalid but it skips validation instead"
56+
!isValid
57+
}
58+
59+
@Rollback
60+
void "calling validate with property list after save should validate again on explicit flush"() {
61+
// Save but don't flush, this causes the new author to have skipValidate = true
62+
Author author = new Author(name: 'Aaron').save(failOnError: true)
63+
64+
when: "validate is called again with a property list"
65+
author.name = "ThisNameIsTooLong"
66+
Author.withSession { session ->
67+
session.flush()
68+
}
69+
70+
then:
71+
author.hasErrors()
72+
}
73+
74+
@Rollback
75+
void "calling validate with no list after save should validate again"() {
76+
// Save but don't flush, this causes the new author to have skipValidate = true
77+
Author author = new Author(name: 'Aaron').save(failOnError: true)
78+
79+
when: "validate is called again without any parameters"
80+
author.name = "ThisNameIsTooLong"
81+
def isValid = author.validate()
82+
83+
then: "this works since validate without params doesn't honor skipValidate for some reason"
84+
!isValid
85+
}
86+
}
87+
88+
@Entity
89+
class Author {
90+
String name
91+
92+
static constraints = {
93+
name(nullable: false, maxSize: 8, validator: { val, obj ->
94+
if(val == "false") {
95+
return "name.invalid"
96+
}
97+
98+
println "Validate called"
99+
true
100+
})
101+
}
102+
}

0 commit comments

Comments
 (0)