Skip to content

Commit f7a04dc

Browse files
Tyler Van Gorderschauder
authored andcommitted
DATAJDBC-219 - Implements optimistic record locking.
Optimistic locking is based on a numeric attribute annotated with `@Version` on the aggregate root. That attribute is increased before any save operation and checked during updates to ensure that the database state hasn't changed since loading the aggregate. Original pull request: #166.
1 parent 8d0441c commit f7a04dc

24 files changed

+773
-89
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.HashSet;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Optional;
2526
import java.util.Set;
2627
import java.util.function.BiConsumer;
2728

@@ -33,6 +34,7 @@
3334
import org.springframework.data.relational.core.conversion.DbAction;
3435
import org.springframework.data.relational.core.conversion.Interpreter;
3536
import org.springframework.data.relational.core.conversion.RelationalConverter;
37+
import org.springframework.data.relational.core.conversion.RelationalEntityVersionUtils;
3638
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3739
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3840
import org.springframework.data.util.Pair;
@@ -45,6 +47,7 @@
4547
*
4648
* @author Jens Schauder
4749
* @author Mark Paluch
50+
* @author Tyler Van Gorder
4851
* @since 1.2
4952
*/
5053
class AggregateChangeExecutor {
@@ -60,7 +63,6 @@ class AggregateChangeExecutor {
6063
this.context = converter.getMappingContext();
6164
}
6265

63-
@SuppressWarnings("unchecked")
6466
<T> void execute(AggregateChange<T> aggregateChange) {
6567

6668
List<DbAction<?>> actions = new ArrayList<>();
@@ -70,17 +72,42 @@ <T> void execute(AggregateChange<T> aggregateChange) {
7072
actions.add(action);
7173
});
7274

73-
T newRoot = (T) populateIdsIfNecessary(actions);
74-
75+
T newRoot = populateIdsIfNecessary(actions);
7576
if (newRoot != null) {
77+
newRoot = populateRootVersionIfNecessary(newRoot, actions);
7678
aggregateChange.setEntity(newRoot);
7779
}
7880
}
7981

82+
@SuppressWarnings("unchecked")
83+
@Nullable
84+
private <T> T populateRootVersionIfNecessary(T newRoot, List<DbAction<?>> actions) {
85+
86+
// Does the root entity have a version attribute?
87+
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) context
88+
.getRequiredPersistentEntity(newRoot.getClass());
89+
if (!persistentEntity.hasVersionProperty()) {
90+
return newRoot;
91+
}
92+
93+
// Find the root action
94+
Optional<DbAction<?>> rootAction = actions.parallelStream().filter(action -> action instanceof DbAction.WithVersion)
95+
.findFirst();
96+
97+
if (!rootAction.isPresent()) {
98+
// This really should never happen.
99+
return newRoot;
100+
}
101+
DbAction.WithVersion<T> versionAction = (DbAction.WithVersion<T>) rootAction.get();
102+
103+
return RelationalEntityVersionUtils.setVersionNumberOnEntity(newRoot,
104+
versionAction.getNextVersion(), persistentEntity, converter);
105+
}
106+
80107
@Nullable
81-
private Object populateIdsIfNecessary(List<DbAction<?>> actions) {
108+
private <T> T populateIdsIfNecessary(List<DbAction<?>> actions) {
82109

83-
Object newRoot = null;
110+
T newRoot = null;
84111

85112
// have the actions so that the inserts on the leaves come first.
86113
List<DbAction<?>> reverseActions = new ArrayList<>(actions);
@@ -102,15 +129,15 @@ private Object populateIdsIfNecessary(List<DbAction<?>> actions) {
102129
if (newEntity != ((DbAction.WithGeneratedId<?>) action).getEntity()) {
103130

104131
if (action instanceof DbAction.Insert) {
105-
DbAction.Insert insert = (DbAction.Insert) action;
132+
DbAction.Insert<?> insert = (DbAction.Insert<?>) action;
106133

107-
Pair qualifier = insert.getQualifier();
134+
Pair<?, ?> qualifier = insert.getQualifier();
108135

109136
cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
110137
qualifier == null ? null : qualifier.getSecond(), newEntity);
111138

112139
} else if (action instanceof DbAction.InsertRoot) {
113-
newRoot = newEntity;
140+
newRoot = (T) newEntity;
114141
}
115142
}
116143
}
@@ -140,7 +167,7 @@ private <S> Object setIdAndCascadingProperties(DbAction.WithGeneratedId<S> actio
140167
}
141168

142169
@SuppressWarnings("unchecked")
143-
private PersistentPropertyPath getRelativePath(DbAction action, PersistentPropertyPath pathToValue) {
170+
private PersistentPropertyPath<?> getRelativePath(DbAction<?> action, PersistentPropertyPath<?> pathToValue) {
144171

145172
if (action instanceof DbAction.Insert) {
146173
return pathToValue.getExtensionForBaseOf(((DbAction.Insert) action).getPropertyPath());

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import lombok.RequiredArgsConstructor;
1919

20-
import java.util.Collections;
2120
import java.util.Map;
2221

2322
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
@@ -34,6 +33,8 @@
3433
import org.springframework.data.relational.core.conversion.DbAction.Update;
3534
import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot;
3635
import org.springframework.data.relational.core.conversion.Interpreter;
36+
import org.springframework.data.relational.core.conversion.RelationalConverter;
37+
import org.springframework.data.relational.core.conversion.RelationalEntityVersionUtils;
3738
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
3839
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3940
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -48,11 +49,13 @@
4849
* @author Jens Schauder
4950
* @author Mark Paluch
5051
* @author Myeonghyeon Lee
52+
* @author Tyler Van Gorder
5153
*/
5254
@RequiredArgsConstructor
5355
class DefaultJdbcInterpreter implements Interpreter {
5456

5557
public static final String UPDATE_FAILED = "Failed to update entity [%s]. Id [%s] not found in database.";
58+
private final RelationalConverter converter;
5659
private final RelationalMappingContext context;
5760
private final DataAccessStrategy accessStrategy;
5861

@@ -62,21 +65,40 @@ class DefaultJdbcInterpreter implements Interpreter {
6265
*/
6366
@Override
6467
public <T> void interpret(Insert<T> insert) {
65-
6668
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), getParentKeys(insert));
67-
6869
insert.setGeneratedId(id);
6970
}
7071

72+
@SuppressWarnings("unchecked")
73+
private <T> RelationalPersistentEntity<T> getRequiredPersistentEntity(Class<T> type) {
74+
return (RelationalPersistentEntity<T>) context.getRequiredPersistentEntity(type);
75+
}
76+
7177
/*
7278
* (non-Javadoc)
7379
* @see org.springframework.data.relational.core.conversion.Interpreter#interpret(org.springframework.data.relational.core.conversion.DbAction.InsertRoot)
7480
*/
7581
@Override
7682
public <T> void interpret(InsertRoot<T> insert) {
7783

78-
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Collections.emptyMap());
79-
insert.setGeneratedId(id);
84+
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(insert.getEntityType());
85+
86+
if (persistentEntity.hasVersionProperty()) {
87+
// The interpreter is responsible for setting the initial version on the entity prior to calling insert.
88+
Number version = RelationalEntityVersionUtils.getVersionNumberFromEntity(insert.getEntity(), persistentEntity,
89+
converter);
90+
if (version != null && version.longValue() > 0) {
91+
throw new IllegalArgumentException("The entity cannot be inserted because it already has a version.");
92+
}
93+
T rootEntity = RelationalEntityVersionUtils.setVersionNumberOnEntity(insert.getEntity(), 1, persistentEntity,
94+
converter);
95+
Object id = accessStrategy.insert(rootEntity, insert.getEntityType(), Identifier.empty());
96+
insert.setNextVersion(1);
97+
insert.setGeneratedId(id);
98+
} else {
99+
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty());
100+
insert.setGeneratedId(id);
101+
}
80102
}
81103

82104
/*
@@ -100,10 +122,31 @@ public <T> void interpret(Update<T> update) {
100122
@Override
101123
public <T> void interpret(UpdateRoot<T> update) {
102124

103-
if (!accessStrategy.update(update.getEntity(), update.getEntityType())) {
104-
105-
throw new IncorrectUpdateSemanticsDataAccessException(
106-
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
125+
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(update.getEntityType());
126+
127+
if (persistentEntity.hasVersionProperty()) {
128+
// If the root aggregate has a version property, increment it.
129+
Number previousVersion = RelationalEntityVersionUtils.getVersionNumberFromEntity(update.getEntity(),
130+
persistentEntity, converter);
131+
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null.");
132+
133+
T rootEntity = RelationalEntityVersionUtils.setVersionNumberOnEntity(update.getEntity(),
134+
previousVersion.longValue() + 1, persistentEntity,
135+
converter);
136+
137+
if (accessStrategy.updateWithVersion(rootEntity, update.getEntityType(), previousVersion)) {
138+
// Successful update, set the in-memory version on the action.
139+
update.setNextVersion(previousVersion);
140+
} else {
141+
throw new IncorrectUpdateSemanticsDataAccessException(
142+
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
143+
}
144+
} else {
145+
if (!accessStrategy.update(update.getEntity(), update.getEntityType())) {
146+
147+
throw new IncorrectUpdateSemanticsDataAccessException(
148+
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
149+
}
107150
}
108151
}
109152

@@ -135,7 +178,16 @@ public <T> void interpret(Delete<T> delete) {
135178
*/
136179
@Override
137180
public <T> void interpret(DeleteRoot<T> delete) {
138-
accessStrategy.delete(delete.getRootId(), delete.getEntityType());
181+
182+
if (delete.getEntity() != null) {
183+
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(delete.getEntityType());
184+
if (persistentEntity.hasVersionProperty()) {
185+
accessStrategy.deleteWithVersion(delete.getEntity(), delete.getEntityType());
186+
return;
187+
}
188+
}
189+
190+
accessStrategy.delete(delete.getId(), delete.getEntityType());
139191
}
140192

141193
/*
@@ -177,13 +229,13 @@ private Object getParentId(DbAction.WithDependingOn<?> action) {
177229
PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(context, action.getPropertyPath());
178230
PersistentPropertyPathExtension idPath = path.getIdDefiningParentPath();
179231

180-
DbAction.WithEntity idOwningAction = getIdOwningAction(action, idPath);
232+
DbAction.WithEntity<?> idOwningAction = getIdOwningAction(action, idPath);
181233

182234
return getIdFrom(idOwningAction);
183235
}
184236

185-
@SuppressWarnings("unchecked")
186-
private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, PersistentPropertyPathExtension idPath) {
237+
private DbAction.WithEntity<?> getIdOwningAction(DbAction.WithEntity<?> action,
238+
PersistentPropertyPathExtension idPath) {
187239

188240
if (!(action instanceof DbAction.WithDependingOn)) {
189241

@@ -193,7 +245,7 @@ private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, Persis
193245
return action;
194246
}
195247

196-
DbAction.WithDependingOn withDependingOn = (DbAction.WithDependingOn) action;
248+
DbAction.WithDependingOn<?> withDependingOn = (DbAction.WithDependingOn<?>) action;
197249

198250
if (idPath.matches(withDependingOn.getPropertyPath())) {
199251
return action;
@@ -202,7 +254,7 @@ private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, Persis
202254
return getIdOwningAction(withDependingOn.getDependingOn(), idPath);
203255
}
204256

205-
private Object getIdFrom(DbAction.WithEntity idOwningAction) {
257+
private Object getIdFrom(DbAction.WithEntity<?> idOwningAction) {
206258

207259
if (idOwningAction instanceof DbAction.WithGeneratedId) {
208260

@@ -221,4 +273,5 @@ private Object getIdFrom(DbAction.WithEntity idOwningAction) {
221273

222274
return identifier;
223275
}
276+
224277
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,20 @@
3131
import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter;
3232
import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter;
3333
import org.springframework.data.relational.core.conversion.RelationalEntityUpdateWriter;
34-
import org.springframework.data.relational.core.conversion.RelationalEntityWriter;
3534
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3635
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
37-
import org.springframework.data.relational.core.mapping.event.*;
36+
import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback;
37+
import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent;
38+
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
39+
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
40+
import org.springframework.data.relational.core.mapping.event.AfterSaveCallback;
41+
import org.springframework.data.relational.core.mapping.event.AfterSaveEvent;
42+
import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
43+
import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback;
44+
import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent;
45+
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
46+
import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
47+
import org.springframework.data.relational.core.mapping.event.Identifier;
3848
import org.springframework.data.relational.core.mapping.event.Identifier.Specified;
3949
import org.springframework.lang.Nullable;
4050
import org.springframework.util.Assert;
@@ -51,10 +61,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
5161

5262
private final ApplicationEventPublisher publisher;
5363
private final RelationalMappingContext context;
54-
private final RelationalConverter converter;
5564
private final Interpreter interpreter;
5665

57-
private final RelationalEntityWriter jdbcEntityWriter;
5866
private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter;
5967
private final RelationalEntityInsertWriter jdbcEntityInsertWriter;
6068
private final RelationalEntityUpdateWriter jdbcEntityUpdateWriter;
@@ -83,14 +91,12 @@ public JdbcAggregateTemplate(ApplicationContext publisher, RelationalMappingCont
8391

8492
this.publisher = publisher;
8593
this.context = context;
86-
this.converter = converter;
8794
this.accessStrategy = dataAccessStrategy;
8895

89-
this.jdbcEntityWriter = new RelationalEntityWriter(context);
9096
this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context);
9197
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
9298
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
93-
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
99+
this.interpreter = new DefaultJdbcInterpreter(converter, context, accessStrategy);
94100

95101
this.executor = new AggregateChangeExecutor(interpreter, converter);
96102

@@ -115,15 +121,12 @@ public JdbcAggregateTemplate(ApplicationEventPublisher publisher, RelationalMapp
115121

116122
this.publisher = publisher;
117123
this.context = context;
118-
this.converter = converter;
119124
this.accessStrategy = dataAccessStrategy;
120125

121-
this.jdbcEntityWriter = new RelationalEntityWriter(context);
122126
this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context);
123127
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
124128
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
125-
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
126-
129+
this.interpreter = new DefaultJdbcInterpreter(converter, context, accessStrategy);
127130
this.executor = new AggregateChangeExecutor(interpreter, converter);
128131
}
129132

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ public <S> boolean update(S instance, Class<S> domainType) {
6868
return collect(das -> das.update(instance, domainType));
6969
}
7070

71+
/*
72+
* (non-Javadoc)
73+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#updateWithVersion(java.lang.Object, java.lang.Class, java.lang.Number)
74+
*/
75+
@Override
76+
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
77+
return collect(das -> das.updateWithVersion(instance, domainType, previousVersion));
78+
}
79+
7180
/*
7281
* (non-Javadoc)
7382
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class)
@@ -77,6 +86,15 @@ public void delete(Object id, Class<?> domainType) {
7786
collectVoid(das -> das.delete(id, domainType));
7887
}
7988

89+
/*
90+
* (non-Javadoc)
91+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteInstance(java.lang.Object, java.lang.Class)
92+
*/
93+
@Override
94+
public <T> void deleteWithVersion(T instance, Class<T> domainType) {
95+
collectVoid(das -> das.deleteWithVersion(instance, domainType));
96+
}
97+
8098
/*
8199
* (non-Javadoc)
82100
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath)

0 commit comments

Comments
 (0)