Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

DATAMONGO-279 - optimistic locking based on version field #11

Closed
wants to merge 2 commits into from

2 participants

Patryk Wasik Oliver Gierke
Patryk Wasik

This pull request contains optimistic locking implementation based on version field.

Changes:

added Version annotation with points to a version field
change save method on MongoTemplet to detect first time save or update.
If first save occure ((isNew() == true) and entity contains version filed then set this field to 0 and perform normal save.
If update occure (isNew() == false) then perform update with query by id and version and inc(version filed by one), then check writeConcern.getN() and throw OptimisticLockingFailureException if no documents was updated.

Any comments will be welcome.

Oliver Gierke
Owner

Very cool stuff. A tiny thing though: would you mind adding some code that binds the updated version to the current entity? Currently you have to manually load the entity to see the updated version number.

Another thought: would it make sense to update the version property in our code and then du a plain update with the former version constraint? That would solve the binding issue of the new version and probably simplify the construction of the update object a little as you don't have to add an inc(…) operation and manually exclude the version property from the update. As we change a single document only, the operation should be atomic anyway.

Patryk Wasik

Change completed.
Hope we don't go over long value in version :)
or do you have any other incrementation solution ?

Oliver Gierke
Owner

That looks pretty good now. I'll have a detailled look tomorrow, brush it up and might even integrate it already. Would make a nice a addition to the upcoming GA release. Would you mind filling out the contributors agreement and comment the id you get assigned? Thanks a ton already!

Patryk Wasik

My confirmation number is: 33320120907012713

Oliver Gierke

What's your name actually? Need it to polish up the JavaDoc (author tags etc.).

Patryk Wasik

Ah ok, I updated my account info. So it is Patryk Wasik.

Oliver Gierke

Polished and merged into master to be included in 1.1 GA. I've removed the MongoOperations enhancements and inlined the modified update(…) method. Added event triggering according to the ones used to from doSave(…).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 312 additions and 5 deletions.
  1. +1 −1  spring-data-mongodb/pom.xml
  2. +2 −0  spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
  3. +43 −2 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  4. +31 −1 ...ng-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
  5. +8 −0 ...-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
  6. +4 −0 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
  7. +9 −0 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
  8. +20 −1 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SimpleMongoMappingContext.java
  9. +18 −0 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Version.java
  10. +14 −0 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
  11. +26 −0 ...ng-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
  12. +31 −0 ...ongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
  13. +37 −0 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  14. +68 −0 ...ata-mongodb/src/test/java/org/springframework/data/mongodb/core/PersonWithVersionPropertyOfTypeInteger.java
2  spring-data-mongodb/pom.xml
View
@@ -181,7 +181,7 @@
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>maven-apt-plugin</artifactId>
- <version>1.0.2</version>
+ <version>1.0.4</version>
<executions>
<execution>
<phase>generate-test-sources</phase>
2  spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
View
@@ -657,6 +657,8 @@
* @return the WriteResult which lets you access the results of the previous write.
*/
WriteResult updateFirst(Query query, Update update, Class<?> entityClass);
+
+ WriteResult updateFirst(Query query, Object object, Class<?> entityClass);
/**
* Updates the first object that is found in the specified collection that matches the query document criteria with
45 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
View
@@ -44,6 +44,7 @@
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.mapping.PersistentEntity;
@@ -703,7 +704,31 @@ public void save(Object objectToSave) {
}
public void save(Object objectToSave, String collectionName) {
- doSave(collectionName, objectToSave, this.mongoConverter);
+ MongoPersistentEntity<?> mongoPersistentEntity = getPersistentEntity(objectToSave.getClass());
+ if(mongoPersistentEntity.hasVersion()){
+ BeanWrapper<PersistentEntity<Object,?>, Object> beanWrapper = BeanWrapper.create(objectToSave, this.mongoConverter.getConversionService());
+ Object id = beanWrapper.getProperty(mongoPersistentEntity.getIdProperty());
+ if(id == null){
+ beanWrapper.setProperty(mongoPersistentEntity.getVersionProperty(), 0);
+ doSave(collectionName, objectToSave, this.mongoConverter);
+ } else {
+ Query query = getUpdateVersionQuery(id,
+ beanWrapper.getProperty(mongoPersistentEntity.getVersionProperty()),mongoPersistentEntity);
+
+ Number number = (Number) beanWrapper.getProperty(mongoPersistentEntity.getVersionProperty());
+ beanWrapper.setProperty(mongoPersistentEntity.getVersionProperty(), number.longValue()+1);
+
+ updateFirst( query,
+ objectToSave, objectToSave.getClass());
+ }
+ } else {
+ doSave(collectionName, objectToSave, this.mongoConverter);
+ }
+ }
+
+ private Query getUpdateVersionQuery(Object id, Object version,MongoPersistentEntity<?> mongoPersistentEntity) {
+ return new Query( Criteria.where(mongoPersistentEntity.getIdProperty().getName()).is(id)
+ .and(mongoPersistentEntity.getVersionProperty().getName()).is(version));
}
protected <T> void doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
@@ -816,6 +841,16 @@ public WriteResult updateFirst(Query query, Update update, Class<?> entityClass)
public WriteResult updateFirst(final Query query, final Update update, final String collectionName) {
return doUpdate(collectionName, query, update, null, false, false);
}
+
+ public WriteResult updateFirst(final Query query, final Object object,
+ Class<?> entityClass) {
+
+ BasicDBObject dbObject = new BasicDBObject();
+ this.mongoConverter.write(object, dbObject);
+
+ return doUpdate(determineCollectionName(entityClass), query,
+ Update.fromDBObject(dbObject, ID), entityClass, false, false);
+ }
public WriteResult updateMulti(Query query, Update update, Class<?> entityClass) {
return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, true);
@@ -835,9 +870,10 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
DBObject queryObj = query == null ? new BasicDBObject()
: mapper.getMappedObject(query.getQueryObject(), entity);
+
DBObject updateObj = update == null ? new BasicDBObject() : mapper.getMappedObject(update.getUpdateObject(),
entity);
-
+
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("calling update using query: " + queryObj + " and update: " + updateObj + " in collection: "
+ collectionName);
@@ -852,6 +888,11 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
} else {
wr = collection.update(queryObj, updateObj, upsert, multi, writeConcernToUse);
}
+ if(null != entity && entity.hasVersion() && !multi){
+ if(wr.getN() == 0){
+ throw new OptimisticLockingFailureException("Optimistic lock exception on saveing entity: "+updateObj.toMap().toString());
+ }
+ }
handleAnyWriteResultErrors(wr, queryObj, "update with '" + updateObj + "'");
return wr;
}
32 ...data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
View
@@ -25,6 +25,7 @@
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BasicPersistentEntity;
+import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.Expression;
@@ -46,6 +47,8 @@
private final String collection;
private final SpelExpressionParser parser;
private final StandardEvaluationContext context;
+
+ private MongoPersistentProperty versionProperty;
/**
* Creates a new {@link BasicMongoPersistentEntity} with the given {@link TypeInformation}. Will default the
@@ -70,6 +73,25 @@ public BasicMongoPersistentEntity(TypeInformation<T> typeInformation) {
this.collection = fallback;
}
}
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.MutablePersistentEntity#addPersistentProperty(P)
+ */
+ @Override
+ public void addPersistentProperty(MongoPersistentProperty property) {
+ if (property.isVersion()) {
+ if (this.versionProperty != null) {
+ throw new MappingException(
+ String.format(
+ "Attempt to add version property %s but already have property %s registered "
+ + "as version. Check your mapping configuration!",
+ property.getField(), versionProperty.getField()));
+ }
+ this.versionProperty = property;
+ }
+ super.addPersistentProperty(property);
+ }
/*
* (non-Javadoc)
@@ -91,6 +113,14 @@ public String getCollection() {
Expression expression = parser.parseExpression(collection, ParserContext.TEMPLATE_EXPRESSION);
return expression.getValue(context, String.class);
}
+
+ public MongoPersistentProperty getVersionProperty() {
+ return versionProperty;
+ }
+
+ public boolean hasVersion() {
+ return getVersionProperty() != null;
+ }
/**
* {@link Comparator} implementation inspecting the {@link MongoPersistentProperty}'s order.
@@ -117,5 +147,5 @@ public int compare(MongoPersistentProperty o1, MongoPersistentProperty o2) {
return o1.getFieldOrder() - o2.getFieldOrder();
}
- }
+ }
}
8 ...ta-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java
View
@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.mapping;
import java.beans.PropertyDescriptor;
+import java.io.ObjectInputStream.GetField;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.HashSet;
@@ -142,4 +143,11 @@ public boolean isDbReference() {
public DBRef getDBRef() {
return getField().getAnnotation(DBRef.class);
}
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.core.mapping.MongoPersistentProperty#isVersion()
+ */
+ public boolean isVersion() {
+ return getField().isAnnotationPresent(Version.class);
+ }
}
4 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
View
@@ -9,4 +9,8 @@
public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersistentProperty> {
String getCollection();
+
+ MongoPersistentProperty getVersionProperty();
+
+ boolean hasVersion();
}
9 ...ng-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
View
@@ -54,6 +54,15 @@
* @return
*/
DBRef getDBRef();
+
+
+ /**
+ * Returns whether the property is {@link Version}.
+ *
+ * @return
+ */
+ boolean isVersion();
+
/**
* Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name.
21 ...-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SimpleMongoMappingContext.java
View
@@ -112,7 +112,15 @@ public boolean isDbReference() {
public DBRef getDBRef() {
return null;
}
- }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.core.mapping.MongoPersistentProperty#isVersion()
+ */
+ public boolean isVersion() {
+ return false;
+ }
+
+ }
static class SimpleMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty> implements
MongoPersistentEntity<T> {
@@ -130,5 +138,16 @@ public SimpleMongoPersistentEntity(TypeInformation<T> information) {
public String getCollection() {
return MongoCollectionUtils.getPreferredCollectionName(getType());
}
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.core.mapping.MongoPersistentEntity#getVersionProperty()
+ */
+ public MongoPersistentProperty getVersionProperty() {
+ return null;
+ }
+
+ public boolean hasVersion() {
+ return false;
+ }
}
}
18 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Version.java
View
@@ -0,0 +1,18 @@
+package org.springframework.data.mongodb.core.mapping;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Specifies the version field that serves as its optimistic lock value.
+ *
+ */
+@Documented
+@Target({ FIELD })
+@Retention(RUNTIME)
+public @interface Version {
+
+}
14 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
View
@@ -15,8 +15,10 @@
*/
package org.springframework.data.mongodb.core.query;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -39,6 +41,18 @@
public static Update update(String key, Object value) {
return new Update().set(key, value);
}
+
+ public static Update fromDBObject(DBObject object, String... exclude) {
+ Update update = new Update();
+ List<String> excludeList = Arrays.asList(exclude);
+ for (String key : object.keySet()) {
+ if(excludeList.contains(key)) {
+ continue;
+ }
+ update.set(key, object.get(key));
+ }
+ return update;
+ }
/**
* Update using the $set update modifier
26 ...data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java
View
@@ -39,4 +39,30 @@
* @return
*/
String getIdAttribute();
+
+
+ /**
+ * Returns the attribute that the version will be persisted to.
+ *
+ * @return
+ */
+ String getVersionAttribute();
+
+
+ /**
+ * Returns the entity version.
+ *
+ * @return
+ */
+ Object getVersion(T entity);
+
+
+
+ /**
+ * Returns whether entity has version.
+ *
+ * @param entity
+ * @return
+ */
+ boolean hasVersion(T entity);
}
31 ...odb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java
View
@@ -71,6 +71,30 @@ public ID getId(T entity) {
throw new RuntimeException(e);
}
}
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.query.MongoEntityInformation#getVersion(T)
+ */
+ public Object getVersion(T entity) {
+ MongoPersistentProperty versionProperty = entityMetadata.getVersionProperty();
+ try {
+ return BeanWrapper.create(entity, null).getProperty(versionProperty);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.query.MongoEntityInformation#hasVersion(T)
+ */
+ public boolean hasVersion(T entity) {
+ try {
+ getVersion(entity);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
/* (non-Javadoc)
* @see org.springframework.data.repository.support.EntityInformation#getIdType()
@@ -93,4 +117,11 @@ public String getCollectionName() {
public String getIdAttribute() {
return entityMetadata.getIdProperty().getName();
}
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.repository.MongoEntityInformation#getVersionAttribute()
+ */
+ public String getVersionAttribute() {
+ return entityMetadata.getVersionProperty().getName();
+ }
}
37 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
View
@@ -40,6 +40,7 @@
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
@@ -134,6 +135,7 @@ protected void cleanDb() {
template.dropCollection(PersonWithIdPropertyOfPrimitiveInt.class);
template.dropCollection(PersonWithIdPropertyOfTypeLong.class);
template.dropCollection(PersonWithIdPropertyOfPrimitiveLong.class);
+ template.dropCollection(PersonWithVersionPropertyOfTypeInteger.class);
template.dropCollection(TestClass.class);
template.dropCollection(Sample.class);
template.dropCollection(MyPerson.class);
@@ -1262,6 +1264,41 @@ public void exceutesBasicQueryCorrectly() {
assertThat(result, hasSize(1));
assertThat(result.get(0), hasProperty("name", is("Oleg")));
}
+
+ @Test(expected = OptimisticLockingFailureException.class)
+ public void optimisticLockingHandling() {
+
+ //Init version
+ PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
+ person.setAge(29);
+ person.setFirstName("Patryk");
+ template.save(person);
+
+ List<PersonWithVersionPropertyOfTypeInteger> result = template.findAll(PersonWithVersionPropertyOfTypeInteger.class);
+
+ assertThat(result, hasSize(1));
+ assertThat(result.get(0), hasProperty("version", is(0)));
+
+ //Version change
+ person = result.get(0);
+ person.setFirstName("Patryk2");
+
+ template.save(person);
+
+ assertThat(person, hasProperty("version",is(1)));
+
+ result = mappingTemplate.findAll(PersonWithVersionPropertyOfTypeInteger.class);
+
+ assertThat(result, hasSize(1));
+ assertThat(result.get(0), hasProperty("version", is(1)));
+
+ //Optimistic lock exception
+ person.setVersion(0);
+ person.setFirstName("Patryk3");
+
+ template.save(person);
+
+ }
static class MyId {
68 ...-mongodb/src/test/java/org/springframework/data/mongodb/core/PersonWithVersionPropertyOfTypeInteger.java
View
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2011 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * http://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 org.springframework.data.mongodb.core;
+
+import org.springframework.data.mongodb.core.mapping.Version;
+
+public class PersonWithVersionPropertyOfTypeInteger {
+
+ private String id;
+
+ private String firstName;
+
+ private int age;
+
+ @Version
+ private Integer version;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ @Override
+ public String toString() {
+ return "PersonWithVersionPropertyOfTypeInteger [id=" + id + ", firstName=" + firstName + ", age=" + age + ", version="+ version + "]";
+ }
+
+}
Something went wrong with that request. Please try again.