Skip to content

Commit 2e18b3f

Browse files
committed
Verifying that target is not transient before accessing its id
1 parent 80cca6b commit 2e18b3f

File tree

2 files changed

+138
-1
lines changed

2 files changed

+138
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package grails.gorm.tests
2+
3+
import grails.persistence.Entity
4+
import org.grails.datastore.mapping.proxy.EntityProxy
5+
6+
class BuiltinUniqueConstraintWorksWithTargetProxiesConstraintsSpec extends GormDatastoreSpec {
7+
8+
void "modified domains object works as expected"() {
9+
given: "I have a Domain OBJECT"
10+
final Referenced object = new Referenced(name: "object").save(failOnError: true)
11+
assert !(object instanceof EntityProxy)
12+
13+
and: "I have another Domain OBJECT with the same name"
14+
final Referenced another = new Referenced(name: "object")
15+
assert !(object instanceof EntityProxy)
16+
17+
when: "I try to validate the another object"
18+
another.validate()
19+
20+
then: "another should have an error on name because it is duplicated"
21+
another.hasErrors()
22+
another.errors.hasFieldErrors("name")
23+
another.errors.getFieldError("name").codes.contains("unique.name")
24+
25+
cleanup:
26+
object?.delete(flush: true)
27+
}
28+
29+
void "modified Referenced domains object works as expected"() {
30+
given: "I have a Domain OBJECT"
31+
final Referenced object = new Referenced(name: "object").save(failOnError: true)
32+
assert !(object instanceof EntityProxy)
33+
and: "a root referencing it"
34+
final Root parent = new Root(ref: object).save(failOnError: true)
35+
assert !(parent instanceof EntityProxy)
36+
37+
and: "I have another Domain OBJECT with the same name"
38+
final Referenced anotherReferenced = new Referenced(name: "object")
39+
assert !(object instanceof EntityProxy)
40+
final Root anotherRoot = new Root(ref: anotherReferenced)
41+
assert !(parent instanceof EntityProxy)
42+
43+
when: "I try to validate the another object"
44+
anotherRoot.validate()
45+
46+
then: "another should have an error on name because it is duplicated"
47+
anotherRoot.hasErrors()
48+
anotherRoot.errors.hasFieldErrors("Referenced.name")
49+
anotherRoot.errors.getFieldError("Referenced.name").codes.contains("unique.name")
50+
51+
cleanup:
52+
object?.delete(flush: true)
53+
parent?.delete(flush: true)
54+
}
55+
56+
void "unmodified Referenced proxies object doesnt fail unique constraint checking"() {
57+
given: "I have a Domain OBJECT"
58+
Long ReferencedId, parentId
59+
Root.withNewSession {
60+
Root.withNewTransaction {
61+
final Referenced object = new Referenced(name: "object").save(failOnError: true)
62+
final Root parent = new Root(ref: object).save(failOnError: true)
63+
64+
ReferencedId = object.id
65+
parentId = parent.id
66+
}
67+
}
68+
and:
69+
int tries = 20
70+
while (!Root.exists(parentId) && !Referenced.exists(ReferencedId) && tries-- > 0) {
71+
sleep(50)
72+
}
73+
74+
and: "I access the parent, forcing the Referenced to be initialized"
75+
76+
def parent = Root.findAll()[0]
77+
assert parent.ref instanceof EntityProxy
78+
parent.ref.name == "object"
79+
80+
when: "I try to validate the the parent (which then tries to validate the Referenced)"
81+
parent.validate()
82+
83+
then: "parent.Referenced should not have any errors!"
84+
!parent.hasErrors()
85+
!parent.errors.hasFieldErrors("Referenced.name")
86+
!parent.errors.getFieldError("Referenced.name").codes.contains("unique.name")
87+
88+
cleanup:
89+
90+
Root.withNewSession {
91+
Root.withNewTransaction {
92+
Referenced.get(ReferencedId)?.delete(flush: true)
93+
Root.get(parentId)?.delete(flush: true)
94+
}
95+
}
96+
tries = 20
97+
while (Root.exists(parentId) && Referenced.exists(ReferencedId) && tries-- > 0) {
98+
sleep(50)
99+
}
100+
}
101+
102+
@Override
103+
List getDomainClasses() {
104+
[Referenced, Root]
105+
}
106+
}
107+
108+
@Entity
109+
class Referenced {
110+
111+
String name
112+
113+
static constraints = {
114+
name nullable: false, unique: true
115+
}
116+
}
117+
118+
@Entity
119+
class Root {
120+
121+
Referenced ref
122+
123+
static constraints = {
124+
ref nullable: false
125+
}
126+
127+
static mapping = {
128+
ref lazy: false
129+
id generator: 'snowflake'
130+
}
131+
}

grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/validation/constraints/builtin/UniqueConstraint.groovy

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,13 @@ class UniqueConstraint extends AbstractConstraint {
138138
if (shouldValidate) {
139139
def existingId = detachedCriteria.get()
140140
if (existingId != null) {
141-
def targetId = reflector.getIdentifier(target)
141+
// We are merely verifying that the object is not transient here
142+
def targetId
143+
if (proxyHandler.isProxy(target)) {
144+
targetId = proxyHandler.getIdentifier(target)
145+
} else {
146+
targetId = reflector.getIdentifier(target)
147+
}
142148
if (targetId != existingId) {
143149
def args = [constraintPropertyName, constraintOwningClass, propertyValue] as Object[]
144150
rejectValue(target, errors, "unique", args, getDefaultMessage("default.not.unique.message"))

0 commit comments

Comments
 (0)