NullPointerException on the save method when id is specified with optimistic concurrency

Murillo, Antonio J 20 Reputation points
2023-09-21T19:11:32.34+00:00

Description

My team is attempting to set the id field on a document and are running into a NullPointerException from the ReactiveCosmosTemplate when saving the document off to the DB. We're utilizing optimistic concurrency with the use of @Version on an _etag field. Up until now, we had left the id field on the model unset, so Cosmos was just generating a random GUID for us. However, we now need to set this to a different unique value at the SDK level before saving it off into the database.

What we've found is that we cannot have all of the following at the same time without running into that NullPointerException

  • Enforcement of optimistic concurrency at the SDK level via the use of @Version on an _etag field
  • Use of @JsonInclude(Include.NON_EMPTY) on our persisted model
  • Ability to set the id field from the SDK level (in our code) instead of leaving it unset and having Cosmos generate it for us

Eliminating any one of those three resolves the error.

This appears to be the exact same issue we're running into except it involved @JsonInclude(Include.NON_NULL) (though the NON_EMPTY is very close to the same thing). However, it appears it was closed?:

Am I understanding that correctly?

Intended Resolution

Does anyone see a path forward? Do we know what the resolution of the issue(s) was, or if it could be looked at again? We'd like to be able to use all three of the listed items at the same time.

The Error

at com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate.applyVersioning(ReactiveCosmosTemplate.java:817)
at com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate.upsert(ReactiveCosmosTemplate.java:470)
at com.azure.spring.data.cosmos.repository.support.SimpleReactiveCosmosRepository.save(SimpleReactiveCosmosRepository.java:102)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at com.sun.proxy.$Proxy266.save(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at com.sun.proxy.$Proxy266.save(Unknown Source)
at com.example.service.dao.ExampleDaoService.saveExampleEntity(ExampleDaoService.java:123)
at com.example.service.example.ExampleService.doSomething(ExampleService.java:999)
at com.exampo$$FastClassBySpringCGLIB$$2383bf94.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
...

Code Snippets

Our Data Model


import org.springframework.data.annotation.Version;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.azure.spring.data.cosmos.core.mapping.Container;

@JsonInclude(Include.NON_EMPTY)
@Container(containerName = "my-container")
public class ExampleEntity implements PersistenceEntity {
    
java
@Id
private String id;

@Version
private String _etag;

...

}

Our Dao Layer


@Service
public class ExampleDaoService {

@Autowired 
private ExampleCosmosRepository exampleCosmosRepository;

...

public void saveExampleEntity(ExampleEntity exampleEntity) {
    exampleCosmosReactiveRepository.save(exampleEntity).block();
}

Our Repository Layer


public interface ExampleCosmosReactiveRepository extends ReactiveCosmosRepository<ExampleEntity, String> {
// holds a few query methods

...

}

Setup

  • MacOS
  • Java 11
  • Spring Boot
  • Azure Cosmos DB (Core API)
  • Libraries:
    • spring-boot-starter-parent: 2.6.6
    • spring-cloud.version: 2021.0.1
    • spring-cloud-azure-dependencies: 4.1.0
  • Environment: Local machine
Azure Cosmos DB
Azure Cosmos DB
An Azure NoSQL database service for app development.
1,632 questions
Azure Spring Apps
Azure Spring Apps
An Azure platform as a service for running Spring Boot applications at cloud scale. Previously known as Azure Spring Cloud.
124 questions
{count} votes

Accepted answer
  1. Oury Ba-MSFT 19,101 Reputation points Microsoft Employee
    2023-09-25T19:39:45.1533333+00:00

    @Murillo, Antonio J Thank you for sharing this solution on the forum. This will definitely help other community members with the same issue.

    It will also be more helpful if you could mark this answer as accept so it can easily be found.

    Solution:

    Placing a @JsonInclude(Include.ALWAYS) on the _etag field on the model resolves the error.

    import org.springframework.data.annotation.Version;
    import com.fasterxml.jackson.annotation.JsonInclude.Include;
    import com.azure.spring.data.cosmos.core.mapping.Container;
    
    @JsonInclude(Include.NON_EMPTY)
    @Container(containerName = "my-container")
    public class ExampleEntity implements PersistenceEntity {
    	
    	@Id
    	private String id;
    
        @JsonInclude(Include.ALWAYS)
    	@Version
    	private String _etag;
    
    ...
    
    }
    
    
    

    Regards,

    Oury

    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Murillo, Antonio J 20 Reputation points
    2023-09-25T16:05:22.5633333+00:00

    This was resolved with the potential solution I posted in my comment below my post. Placing a @JsonInclude(Include.ALWAYS) on the _etag field on the model gets rid of the the NullPointerException and still allows you to enforce optimistic concurrency. I raised an issue on the azure-sdk-for-java repository, and the folks triaging confirmed it to be a good path forward.

    
    import org.springframework.data.annotation.Version;
    import com.fasterxml.jackson.annotation.JsonInclude.Include;
    import com.azure.spring.data.cosmos.core.mapping.Container;
    
    @JsonInclude(Include.NON_EMPTY)
    @Container(containerName = "my-container")
    public class ExampleEntity implements PersistenceEntity {
    	
    	@Id
    	private String id;
    
        @JsonInclude(Include.ALWAYS)
    	@Version
    	private String _etag;
    
    ...
    
    }
    
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.