E11000 duplicate key error when doing PUT for modi

2019-05-18 18:57发布

问题:

Update:

According to this question, the author of Spring data rest say, the @Version properties will become ETags in response header. And there are two options for update:

  1. Just PUT without an If-Match header -- enforces overriding whatever is present on the server as the aggregate gets loaded, incoming data mapped onto it and it written back. You still get optimistic locking applied if another client changed the aggregate in the meantime (although an admittedly very short window). If that's the case you'll see a 409 Conflict.

    I am currently using this way for PUT, and a 409 conflict is what I have got. But the exception doesn't seems to be optimistic locking. And there suppose will not have another client changed the aggreate in the meantime.

  2. PUT with an If-Match header - Spring Data REST checks the ETag submitted against the current value of the version property of the aggregate and return a 412 Precondition Failed in case there's a mismatch at that point.

    I have tried to add If-Match header(If-Match: "0" or If-Match: 0), but both of them got 500 Internal server error. The exception is as below and I have check that the resource I PUT is currently version "0" in both db and ETags in response header if I GET it.

    java.lang.NullPointerException: null at org.springframework.data.rest.webmvc.support.ETag.getVersionInformation(ETag.java:192) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na] at org.springframework.data.rest.webmvc.support.ETag.from(ETag.java:76) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na] at org.springframework.data.rest.webmvc.support.ETag.verify(ETag.java:94) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na] at org.springframework.data.rest.webmvc.RepositoryEntityController.putItemResource(RepositoryEntityController.java:410) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_45] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_45] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_45] at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_45] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:222) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:814) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:737) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) ~[spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:969) [spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:882) [spring-webmvc-4.2.4.RELEASE.jar:4.2.4.RELEASE]

According to this question, the author of Spring data rest say, with Spring Data JPA, you need to use @javax.persistence.Version. @org.springframework.data.annotation.Version is the annotation to use for other Spring Data modules.

I have tried to changed to using @javax.persistence.Version, but the version properties will no longer become ETags in header, it will shows in "version" field in response body. In the meantime, if I put without If-Match, it will successful(200), but the version field will be droped away. If I put with If-Match: 0, it will got 412 Precondition Failed. No matter what, I am not using spring-data-jpa, I am using spring-boot-starter-data-rest and spring-boot-starter-data-mongodb. So that, I think that is right way for my @Version will be org.springframework.data.annotation.Version in Spring Data Common.


We have a REST API webapp by Spring Data Rest framework with MongoDB. And there has a @document class DeliveryOrder

@Document(collection = "delivery_orders")
public class DeliveryOrder extends PersistableDocument {

private static final long serialVersionUID = 1L;

@LastModifiedBy
protected String            lastModifiedBy;
@LastModifiedDate
@DateTimeFormat(iso = ISO.DATE_TIME)
protected Instant           lastModifiedDate;
@Version
protected Long              version;

protected Map<String, Object> dyna;

...
}

And the PersistableDocument as below

public abstract class PersistableDocument {

private static final long serialVersionUID = 1L;

@Id
protected String id;

@CreatedBy
protected String createdBy;

@CreatedDate
@DateTimeFormat(iso = ISO.DATE_TIME)
@Indexed(direction = IndexDirection.ASCENDING, unique = false)
protected Instant createdDate;
...
}

If I PUT the resource which already existed (URI: https://${HOST}/${APPLICATION_NAMEAME}/${Object_Id}) will got 409 conflict and the exception as below. But If I have removed the @Version field, it will works as PUT method. It seems not because the _id field is duplicated. Might be some optimistic locking issue?

Any help is appreciated

2016-05-04 17:38:34.413 ERROR 8668 --- [nio-1010-exec-2] 

o.s.d.r.w.RepositoryRestExceptionHandler : Write failed with error code 11000 and error message 'E11000 duplicate key error collection: 56cbfbd323f5496dcc02c579.delivery_orders index: _id_ dup key: { : ObjectId('56d6b81623f54966b07b0587') }'; nested exception is com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'E11000 duplicate key error collection: 56cbfbd323f5496dcc02c579.delivery_orders index: _id_ dup key: { : ObjectId('56d6b81623f54966b07b0587') }'

org.springframework.dao.DuplicateKeyException: Write failed with error code 11000 and error message 'E11000 duplicate key error collection: 56cbfbd323f5496dcc02c579.delivery_orders index: _id_ dup key: { : ObjectId('56d6b81623f54966b07b0587') }'; nested exception is com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'E11000 duplicate key error collection: 56cbfbd323f5496dcc02c579.delivery_orders index: _id_ dup key: { : ObjectId('56d6b81623f54966b07b0587') }'
    at org.springframework.data.mongodb.core.MongoExceptionTranslator.translateExceptionIfPossible(MongoExceptionTranslator.java:71) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.potentiallyConvertRuntimeException(MongoTemplate.java:2060) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.execute(MongoTemplate.java:464) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.insertDBObject(MongoTemplate.java:985) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.doInsert(MongoTemplate.java:798) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.doSaveVersioned(MongoTemplate.java:941) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:925) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:78) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_45]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_45]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_45]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_45]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:483) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:468) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:440) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at com.sun.proxy.$Proxy122.save(Unknown Source) ~[na:na]
    at org.springframework.data.repository.support.CrudRepositoryInvoker.invokeSave(CrudRepositoryInvoker.java:100) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeSave(UnwrappingRepositoryInvokerFactory.java:225) ~[spring-data-rest-core-2.4.1.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.saveAndReturn(RepositoryEntityController.java:491) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.putItemResource(RepositoryEntityController.java:413) ~[spring-data-rest-webmvc-2.4.1.RELEASE.jar:na]
Caused by: com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'E11000 duplicate key error collection: 56cbfbd323f5496dcc02c579.delivery_orders index: _id_ dup key: { : ObjectId('56d6b81623f54966b07b0587') }'
    at com.mongodb.operation.BaseWriteOperation.convertBulkWriteException(BaseWriteOperation.java:236) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.BaseWriteOperation.access$300(BaseWriteOperation.java:60) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.BaseWriteOperation$1.call(BaseWriteOperation.java:146) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.BaseWriteOperation$1.call(BaseWriteOperation.java:133) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.OperationHelper.withConnectionSource(OperationHelper.java:230) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.OperationHelper.withConnection(OperationHelper.java:221) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.BaseWriteOperation.execute(BaseWriteOperation.java:133) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.operation.BaseWriteOperation.execute(BaseWriteOperation.java:60) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.Mongo.execute(Mongo.java:782) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.Mongo$2.execute(Mongo.java:765) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.executeWriteOperation(DBCollection.java:333) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.insert(DBCollection.java:328) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.insert(DBCollection.java:319) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.insert(DBCollection.java:289) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.insert(DBCollection.java:255) ~[mongo-java-driver-3.2.0.jar:na]
    at com.mongodb.DBCollection.insert(DBCollection.java:192) ~[mongo-java-driver-3.2.0.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate$9.doInCollection(MongoTemplate.java:990) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    at org.springframework.data.mongodb.core.MongoTemplate.execute(MongoTemplate.java:462) ~[spring-data-mongodb-1.8.1.RELEASE.jar:na]
    ... 117 common frames omitted

回答1:

I have found the root cause finally. That is because I didn't give the version field in my PUT body, and Spring-data-rest may be considered to do some insert operation for the document, hence, there will have E11000 duplicate key error exception being thrown.

But, the main issue is that I shall not have an implementation for the setter of the version field as below:

public void setVersion(Long version)
{
   this.version = version;
}

The version parameter will be null if there has no version field in the body of your PUT request. Just remove this setter or annotate with @JsonIgnore can solve this problem. Spring-data-rest will take care the rest.



回答2:

An alternative answer that doesn't directly address the OP issue, but others maybe running into.

If you are overriding SDR in a @RepositoryRestController PUT method, it's unlikely that your front end is sending a version property, just as it's unlikely that SPD is sending down a version property.

I'm not sure what the right solution is, but we settled on extracting the etag from the header and putting it on the model before submitting a PUT request to our custom controller.

if (obj && header.etag && _.isInteger(header.etag)) {
  obj.version = parseInt(header.etag, 10);
}