Skip to content

Commit d7c213d

Browse files
authored
Merge pull request #1322 from grails/1307_rebased
Sync $changedProperties in the proxy with the target
2 parents bd802d3 + f590b5c commit d7c213d

File tree

6 files changed

+288
-136
lines changed

6 files changed

+288
-136
lines changed

grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckable.groovy

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ trait DirtyCheckable {
2626
$changedProperties = new LinkedHashMap<String, Object>()
2727
}
2828

29+
/**
30+
* Sync the changes for a given instance with this instance.
31+
*
32+
* @param o a given object
33+
*/
34+
void syncChangedProperties(Object o) {
35+
if (o instanceof DirtyCheckable) {
36+
o.trackChanges($changedProperties)
37+
}
38+
}
39+
40+
/**
41+
* Initialises the changes with the given changes.
42+
*
43+
* @param changedProperties The changes.
44+
*/
45+
void trackChanges(Map<String, Object> changedProperties) {
46+
$changedProperties = changedProperties
47+
}
48+
2949
/**
3050
* @return True if the instance has any changes
3151
*/

grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/proxy/AssociationQueryProxyHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.grails.datastore.mapping.proxy;
1717

1818
import org.grails.datastore.mapping.core.Session;
19+
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable;
1920
import org.grails.datastore.mapping.engine.AssociationQueryExecutor;
2021
import org.grails.datastore.mapping.reflect.FieldEntityAccess;
2122
import org.springframework.cglib.reflect.FastClass;
@@ -80,6 +81,9 @@ protected Object resolveDelegate(Object self) {
8081
if( target == null ) {
8182
throw new DataIntegrityViolationException("Proxy for ["+ proxyClass.getName()+"] for association ["+executor.getIndexedEntity().getName()+"] could not be initialized");
8283
}
84+
if (target instanceof DirtyCheckable) {
85+
((DirtyCheckable) target).syncChangedProperties(self);
86+
}
8387
}
8488
return target;
8589
}

grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/proxy/SessionEntityProxyMethodHandler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.grails.datastore.mapping.proxy;
1717

1818
import org.grails.datastore.mapping.core.Session;
19+
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable;
1920
import org.grails.datastore.mapping.model.PersistentEntity;
2021
import org.grails.datastore.mapping.reflect.FieldEntityAccess;
2122
import org.slf4j.Logger;
@@ -54,7 +55,7 @@ public SessionEntityProxyMethodHandler(Class proxyClass, Session session, Class
5455
@Override
5556
protected Object resolveDelegate(Object self) {
5657
if (target == null) {
57-
initializeTarget();
58+
initializeTarget(self);
5859

5960
// This tends to happen during unit testing if the proxy class is not properly mocked
6061
// and therefore can't be found in the session.
@@ -72,6 +73,13 @@ protected void initializeTarget() {
7273
target = session.retrieve(cls, id);
7374
}
7475

76+
protected void initializeTarget(Object self) {
77+
initializeTarget();
78+
if (target instanceof DirtyCheckable) {
79+
((DirtyCheckable) target).syncChangedProperties(self);
80+
}
81+
}
82+
7583
@Override
7684
protected Object isProxyInitiated(Object self) {
7785
return target != null;
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package grails.gorm.tests
2+
3+
import grails.gorm.annotation.Entity
4+
import org.grails.datastore.mapping.proxy.ProxyHandler
5+
import spock.lang.IgnoreIf
6+
7+
/**
8+
* @author Graeme Rocher
9+
*/
10+
class DirtyCheckingSpec extends GormDatastoreSpec {
11+
12+
ProxyHandler proxyHandler
13+
14+
@Override
15+
List getDomainClasses() {
16+
[Person, TestBook, TestAuthor, Card, CardProfile]
17+
}
18+
19+
def setup() {
20+
proxyHandler = session.getMappingContext().proxyHandler
21+
}
22+
23+
void "Test that dirty checking methods work when changing entities"() {
24+
25+
when: "A new instance is created"
26+
def p = new Person(firstName: "Homer", lastName: "Simpson")
27+
p.save(flush: true)
28+
29+
then: "The instance is not dirty"
30+
!p.isDirty()
31+
!p.isDirty("firstName")
32+
33+
when: "The instance is changed"
34+
p.firstName = "Bart"
35+
36+
then: "The instance is now dirty"
37+
p.isDirty()
38+
p.isDirty("firstName")
39+
p.dirtyPropertyNames == ['firstName']
40+
p.getPersistentValue('firstName') == "Homer"
41+
42+
when: "The instance is loaded from the db"
43+
p.save(flush: true)
44+
session.clear()
45+
p = Person.get(p.id)
46+
47+
then: "The instance is not dirty"
48+
!p.isDirty()
49+
!p.isDirty('firstName')
50+
51+
when: "The instance is changed"
52+
p.firstName = "Lisa"
53+
54+
then: "The instance is dirty"
55+
p.isDirty()
56+
p.isDirty("firstName")
57+
58+
59+
}
60+
61+
void "test relationships not marked dirty when proxies are used"() {
62+
63+
given:
64+
Long bookId = new TestBook(title: 'Martin Fierro', author: new TestAuthor(name: 'Jose Hernandez'))
65+
.save(flush: true)
66+
.id
67+
session.flush()
68+
session.clear()
69+
70+
when:
71+
TestBook book = TestBook.get(bookId)
72+
book.author = book.author
73+
74+
then:
75+
proxyHandler.isProxy(book.author)
76+
!book.isDirty('author')
77+
!book.isDirty()
78+
79+
cleanup:
80+
TestBook.deleteAll()
81+
TestAuthor.deleteAll()
82+
}
83+
84+
void "test relationships not marked dirty when domain objects are used"() {
85+
86+
given:
87+
Long bookId = new TestBook(title: 'Martin Fierro', author: new TestAuthor(name: 'Jose Hernandez'))
88+
.save(flush: true, failOnError: true)
89+
.id
90+
session.flush()
91+
session.clear()
92+
93+
when:
94+
TestBook book = TestBook.get(bookId)
95+
book.author = TestAuthor.get(book.author.id)
96+
97+
then:
98+
!proxyHandler.isProxy(book.author)
99+
!book.isDirty('author')
100+
!book.isDirty()
101+
102+
cleanup:
103+
TestBook.deleteAll()
104+
TestAuthor.deleteAll()
105+
}
106+
107+
void "test relationships are marked dirty when proxies are used but different"() {
108+
given:
109+
Long bookId = new TestBook(title: 'Martin Fierro', author: new TestAuthor(name: 'Jose Hernandez'))
110+
.save(flush: true, failOnError: true)
111+
.id
112+
Long otherAuthorId = new TestAuthor(name: "JD").save(flush: true, failOnError: true).id
113+
session.flush()
114+
session.clear()
115+
116+
when:
117+
TestBook book = TestBook.get(bookId)
118+
book.author = TestAuthor.load(otherAuthorId)
119+
120+
then:
121+
proxyHandler.isProxy(book.author)
122+
book.isDirty('author')
123+
book.isDirty()
124+
125+
cleanup:
126+
TestBook.deleteAll()
127+
TestAuthor.deleteAll()
128+
}
129+
130+
void "test relationships marked dirty when domain objects are used and changed"() {
131+
132+
given:
133+
Long bookId = new TestBook(title: 'Martin Fierro', author: new TestAuthor(name: 'Jose Hernandez'))
134+
.save(flush: true, failOnError: true)
135+
.id
136+
Long otherAuthorId = new TestAuthor(name: "JD").save(flush: true, failOnError: true).id
137+
session.flush()
138+
session.clear()
139+
140+
when:
141+
TestBook book = TestBook.get(bookId)
142+
book.author = TestAuthor.get(otherAuthorId)
143+
144+
then:
145+
!proxyHandler.isProxy(book.author)
146+
book.isDirty('author')
147+
book.isDirty()
148+
149+
cleanup:
150+
TestBook.deleteAll()
151+
TestAuthor.deleteAll()
152+
}
153+
154+
@IgnoreIf({ Boolean.getBoolean("hibernate5.gorm.suite")}) // because one-to-one association loads eagerly in the Hibernate
155+
void "test initialized proxy is not marked as dirty"() {
156+
157+
given:
158+
Card card = new Card(cardNumber: "1111-2222-3333-4444")
159+
card.cardProfile = new CardProfile(fullName: "JD")
160+
card.save(flush: true, failOnError: true)
161+
session.flush()
162+
session.clear()
163+
164+
when:
165+
card = Card.get(card.id)
166+
167+
then:
168+
proxyHandler.isProxy(card.cardProfile)
169+
170+
when:
171+
card.cardProfile.hashCode()
172+
173+
then:
174+
proxyHandler.isInitialized(card.cardProfile)
175+
!card.isDirty()
176+
177+
cleanup:
178+
Card.deleteAll()
179+
CardProfile.deleteAll()
180+
181+
}
182+
183+
}
184+
185+
@Entity
186+
class Card implements Serializable {
187+
188+
Long id
189+
String cardNumber
190+
static hasOne = [cardProfile: CardProfile]
191+
}
192+
193+
@Entity
194+
class CardProfile implements Serializable {
195+
196+
Long id
197+
String fullName
198+
Card card
199+
200+
static constraints = {
201+
card nullable: true
202+
}
203+
204+
}
205+
206+
@Entity
207+
class TestAuthor implements Serializable {
208+
209+
Long id
210+
String name
211+
212+
@Override
213+
boolean equals(o) {
214+
if (!(o instanceof TestAuthor)) return false
215+
if (this.is(o)) return true
216+
TestAuthor that = (TestAuthor) o
217+
if (id !=null && that.id !=null) return id == that.id
218+
return false
219+
}
220+
}
221+
222+
@Entity
223+
class TestBook implements Serializable {
224+
225+
Long id
226+
String title
227+
TestAuthor author
228+
}

0 commit comments

Comments
 (0)