Optimistic Lock implementation based on _etag field (#396)

* Optimistic Lock first implementation

* Ignore warnings on "_etag" field name not matching Java conventions

* Fix JUnit test that was checking on wrong illegal arguments

* Update precondition not met message to match new version

* Precondition is not met to is not met
This commit is contained in:
Domenico Sibilio 2019-09-26 18:40:35 +02:00 коммит произвёл Kushagra Thapar
Родитель a1f7dd3116
Коммит 4f43dcf266
11 изменённых файлов: 403 добавлений и 138 удалений

3
.codacy.yml Normal file
Просмотреть файл

@ -0,0 +1,3 @@
exclude_paths:
- 'src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/Person.java'
- 'src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/support/DocumentDbEntityInformationUnitTest.java'

Просмотреть файл

@ -15,7 +15,7 @@ mvnw clean install
```
## Test
There're 3 profiles: `dev`, `integration-test-azure` and `integration-test-emulator`. Default profile is `dev`. Profile `integration-test-azure` will trigger integration test execution against Azure Cosmos DB. Profile `integration-test-emulator` will trigger integration test execution against [Azure Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator), you need to follow the link to setup emualator before test execution.
There're 3 profiles: `dev`, `integration-test-azure` and `integration-test-emulator`. Default profile is `dev`. Profile `integration-test-azure` will trigger integration test execution against Azure Cosmos DB. Profile `integration-test-emulator` will trigger integration test execution against [Azure Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator), you need to follow the link to setup emulator before test execution.
- Run unit tests
```bash

Просмотреть файл

@ -0,0 +1,31 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for
* license information.
*/
package com.microsoft.azure.spring.data.cosmosdb.common;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* Memoize function computation results
* @author Domenico Sibilio
*
*/
public class Memoizer<I, O> {
private final Map<I, O> cache = new ConcurrentHashMap<>();
private Memoizer() {}
public static <I, O> Function<I, O> memoize(Function<I, O> function) {
return new Memoizer<I, O>().internalMemoize(function);
}
private Function<I, O> internalMemoize(Function<I, O> function) {
return input -> cache.computeIfAbsent(input, function);
}
}

Просмотреть файл

@ -11,6 +11,7 @@ import com.microsoft.azure.documentdb.PartitionKey;
import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingDocumentDbConverter;
import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery;
import com.microsoft.azure.spring.data.cosmosdb.repository.support.DocumentDbEntityInformation;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -20,7 +21,7 @@ public interface DocumentDbOperations {
String getCollectionName(Class<?> entityClass);
DocumentCollection createCollectionIfNotExists(DocumentDbEntityInformation information);
DocumentCollection createCollectionIfNotExists(DocumentDbEntityInformation<?, ?> information);
<T> List<T> findAll(Class<T> entityClass);
@ -38,7 +39,7 @@ public interface DocumentDbOperations {
<T> void upsert(String collectionName, T object, PartitionKey partitionKey);
<T> void deleteById(String collectionName, Object id, PartitionKey partitionKey);
void deleteById(String collectionName, Object id, PartitionKey partitionKey);
void deleteAll(String collectionName, Class<?> domainClass);

Просмотреть файл

@ -6,6 +6,8 @@
package com.microsoft.azure.spring.data.cosmosdb.core;
import com.azure.data.cosmos.AccessCondition;
import com.azure.data.cosmos.AccessConditionType;
import com.azure.data.cosmos.CosmosClient;
import com.azure.data.cosmos.CosmosContainerResponse;
import com.azure.data.cosmos.CosmosItemProperties;
@ -17,6 +19,7 @@ import com.azure.data.cosmos.SqlQuerySpec;
import com.microsoft.azure.documentdb.DocumentCollection;
import com.microsoft.azure.documentdb.PartitionKey;
import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory;
import com.microsoft.azure.spring.data.cosmosdb.common.Memoizer;
import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingDocumentDbConverter;
import com.microsoft.azure.spring.data.cosmosdb.core.generator.CountQueryGenerator;
import com.microsoft.azure.spring.data.cosmosdb.core.generator.FindQuerySpecGenerator;
@ -27,6 +30,9 @@ import com.microsoft.azure.spring.data.cosmosdb.core.query.DocumentQuery;
import com.microsoft.azure.spring.data.cosmosdb.exception.DocumentDBAccessException;
import com.microsoft.azure.spring.data.cosmosdb.repository.support.DocumentDbEntityInformation;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@ -36,15 +42,19 @@ import org.springframework.data.domain.Pageable;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
* @author Domenico Sibilio
*
*/
@Slf4j
public class DocumentDbTemplate implements DocumentDbOperations, ApplicationContextAware {
@ -54,10 +64,12 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
private final String databaseName;
private final CosmosClient cosmosClient;
private Function<Class<?>, DocumentDbEntityInformation<?, ?>> entityInfoCreator =
Memoizer.memoize(this::getDocumentDbEntityInformation);
public DocumentDbTemplate(CosmosDbFactory cosmosDbFactory,
MappingDocumentDbConverter mappingDocumentDbConverter,
String dbName) {
MappingDocumentDbConverter mappingDocumentDbConverter,
String dbName) {
Assert.notNull(cosmosDbFactory, "CosmosDbFactory must not be null!");
Assert.notNull(mappingDocumentDbConverter, "MappingDocumentDbConverter must not be null!");
@ -92,10 +104,10 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final Class<T> domainClass = (Class<T>) objectToSave.getClass();
final CosmosItemResponse response = cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.createItem(originalItem, options)
.onErrorResume(Mono::error)
.block();
.getContainer(collectionName)
.createItem(originalItem, options)
.onErrorResume(Mono::error)
.block();
if (response == null) {
throw new DocumentDBAccessException("Failed to insert item");
@ -125,16 +137,16 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final FeedOptions options = new FeedOptions();
options.enableCrossPartitionQuery(true);
return cosmosClient
.getDatabase(databaseName)
.getContainer(collectionName)
.queryItems(query, options)
.flatMap(cosmosItemFeedResponse -> Mono.justOrEmpty(cosmosItemFeedResponse
.results()
.stream()
.map(cosmosItem -> mappingDocumentDbConverter.read(domainClass, cosmosItem))
.findFirst()))
.onErrorResume(Mono::error)
.blockFirst();
.getDatabase(databaseName)
.getContainer(collectionName)
.queryItems(query, options)
.flatMap(cosmosItemFeedResponse -> Mono.justOrEmpty(cosmosItemFeedResponse
.results()
.stream()
.map(cosmosItem -> mappingDocumentDbConverter.read(domainClass, cosmosItem))
.findFirst()))
.onErrorResume(Mono::error)
.blockFirst();
} catch (Exception e) {
throw new DocumentDBAccessException("findById exception", e);
@ -158,12 +170,13 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final CosmosItemRequestOptions options = new CosmosItemRequestOptions();
options.partitionKey(toCosmosPartitionKey(partitionKey));
applyVersioning(object.getClass(), originalItem, options);
final CosmosItemResponse cosmosItemResponse = cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.upsertItem(originalItem, options)
.onErrorResume(Mono::error)
.block();
.getContainer(collectionName)
.upsertItem(originalItem, options)
.onErrorResume(Mono::error)
.block();
if (cosmosItemResponse == null) {
throw new DocumentDBAccessException("Failed to upsert item");
@ -187,8 +200,8 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final List<CosmosItemProperties> documents = findDocuments(query, domainClass, collectionName);
return documents.stream()
.map(d -> getConverter().read(domainClass, d))
.collect(Collectors.toList());
.map(d -> getConverter().read(domainClass, d))
.collect(Collectors.toList());
}
public void deleteAll(@NonNull String collectionName, @NonNull Class<?> domainClass) {
@ -206,26 +219,26 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
cosmosClient.getDatabase(this.databaseName).getContainer(collectionName).delete().block();
} catch (Exception e) {
throw new DocumentDBAccessException("failed to delete collection: " + collectionName,
e);
e);
}
}
public String getCollectionName(Class<?> domainClass) {
Assert.notNull(domainClass, "domainClass should not be null");
return new DocumentDbEntityInformation<>(domainClass).getCollectionName();
return entityInfoCreator.apply(domainClass).getCollectionName();
}
@Override
public DocumentCollection createCollectionIfNotExists(@NonNull DocumentDbEntityInformation information) {
public DocumentCollection createCollectionIfNotExists(@NonNull DocumentDbEntityInformation<?, ?> information) {
final CosmosContainerResponse response = cosmosClient
.createDatabaseIfNotExists(this.databaseName)
.flatMap(cosmosDatabaseResponse -> cosmosDatabaseResponse
.database()
.createContainerIfNotExists(information.getCollectionName(),
"/" + information.getPartitionKeyFieldName())
.map(cosmosContainerResponse -> cosmosContainerResponse))
.block();
.createDatabaseIfNotExists(this.databaseName)
.flatMap(cosmosDatabaseResponse -> cosmosDatabaseResponse
.database()
.createContainerIfNotExists(information.getCollectionName(),
"/" + information.getPartitionKeyFieldName())
.map(cosmosContainerResponse -> cosmosContainerResponse))
.block();
if (response == null) {
throw new DocumentDBAccessException("Failed to create collection");
}
@ -246,12 +259,12 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final CosmosItemRequestOptions options = new CosmosItemRequestOptions();
options.partitionKey(pk);
cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.getItem(id.toString(), partitionKey)
.delete(options)
.onErrorResume(Mono::error)
.then()
.block();
.getContainer(collectionName)
.getItem(id.toString(), partitionKey)
.delete(options)
.onErrorResume(Mono::error)
.then()
.block();
} catch (Exception e) {
throw new DocumentDBAccessException("deleteById exception", e);
}
@ -275,9 +288,9 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
try {
return findDocuments(query, domainClass, collectionName)
.stream()
.stream()
.map(cosmosItemProperties -> toDomainObject(domainClass, cosmosItemProperties))
.collect(Collectors.toList());
.collect(Collectors.toList());
} catch (Exception e) {
throw new DocumentDBAccessException("Failed to execute find operation from " + collectionName, e);
}
@ -300,7 +313,7 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
*/
@Override
public <T> List<T> delete(@NonNull DocumentQuery query, @NonNull Class<T> domainClass,
@NonNull String collectionName) {
@NonNull String collectionName) {
Assert.notNull(query, "DocumentQuery should not be null.");
Assert.notNull(domainClass, "domainClass should not be null.");
Assert.hasText(collectionName, "collection should not be null, empty or only whitespaces");
@ -308,11 +321,11 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final List<CosmosItemProperties> results = findDocuments(query, domainClass, collectionName);
final List<String> partitionKeyName = getPartitionKeyNames(domainClass);
results.forEach(d -> deleteDocument(d, partitionKeyName, collectionName));
results.forEach(d -> deleteDocument(d, partitionKeyName, collectionName, domainClass));
return results.stream()
.map(d -> getConverter().read(domainClass, d))
.collect(Collectors.toList());
.map(d -> getConverter().read(domainClass, d))
.collect(Collectors.toList());
}
@Override
@ -341,11 +354,11 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query);
final FeedResponse<CosmosItemProperties> feedResponse =
cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.queryItems(sqlQuerySpec, feedOptions)
.next()
.block();
cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.queryItems(sqlQuerySpec, feedOptions)
.next()
.block();
if (feedResponse == null) {
throw new DocumentDBAccessException("Failed to query documents");
@ -366,9 +379,9 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
}
final DocumentDbPageRequest pageRequest = DocumentDbPageRequest.of(pageable.getPageNumber(),
pageable.getPageSize(),
feedResponse.continuationToken(),
query.getSort());
pageable.getPageSize(),
feedResponse.continuationToken(),
query.getSort());
return new PageImpl<>(result, pageRequest, count(query, domainClass, collectionName));
}
@ -391,7 +404,7 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
Assert.hasText(collectionName, "collectionName should not be empty");
final boolean isCrossPartitionQuery =
query.isCrossPartitionQuery(getPartitionKeyNames(domainClass));
query.isCrossPartitionQuery(getPartitionKeyNames(domainClass));
final Long count = getCountValue(query, isCrossPartitionQuery, collectionName);
if (count == null) {
throw new DocumentDBAccessException("Failed to get count for collectionName: " + collectionName);
@ -411,10 +424,10 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
options.enableCrossPartitionQuery(isCrossPartitionQuery);
return executeQuery(querySpec, containerName, options)
.onErrorResume(this::databaseAccessExceptionHandler)
.next()
.map(r -> r.results().get(0).getLong(COUNT_VALUE_KEY))
.block();
.onErrorResume(this::databaseAccessExceptionHandler)
.next()
.map(r -> r.results().get(0).getLong(COUNT_VALUE_KEY))
.block();
}
private <T> Mono<T> databaseAccessExceptionHandler(Throwable e) {
@ -422,15 +435,14 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
}
private Flux<FeedResponse<CosmosItemProperties>> executeQuery(SqlQuerySpec sqlQuerySpec, String collectionName,
FeedOptions options) {
FeedOptions options) {
return cosmosClient.getDatabase(this.databaseName)
.getContainer(collectionName)
.queryItems(sqlQuerySpec, options);
.getContainer(collectionName)
.queryItems(sqlQuerySpec, options);
}
@SuppressWarnings("unchecked")
private List<String> getPartitionKeyNames(Class<?> domainClass) {
final DocumentDbEntityInformation entityInfo = new DocumentDbEntityInformation(domainClass);
final DocumentDbEntityInformation<?, ?> entityInfo = entityInfoCreator.apply(domainClass);
if (entityInfo.getPartitionKeyFieldName() == null) {
return new ArrayList<>();
@ -454,25 +466,26 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
}
private List<CosmosItemProperties> findDocuments(@NonNull DocumentQuery query,
@NonNull Class<?> domainClass,
@NonNull String containerName) {
@NonNull Class<?> domainClass,
@NonNull String containerName) {
final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query);
final boolean isCrossPartitionQuery =
query.isCrossPartitionQuery(getPartitionKeyNames(domainClass));
query.isCrossPartitionQuery(getPartitionKeyNames(domainClass));
final FeedOptions feedOptions = new FeedOptions();
feedOptions.enableCrossPartitionQuery(isCrossPartitionQuery);
return cosmosClient
.getDatabase(this.databaseName)
.getContainer(containerName)
.queryItems(sqlQuerySpec, feedOptions)
.flatMap(cosmosItemFeedResponse -> Flux.fromIterable(cosmosItemFeedResponse.results()))
.collectList()
.block();
.getDatabase(this.databaseName)
.getContainer(containerName)
.queryItems(sqlQuerySpec, feedOptions)
.flatMap(cosmosItemFeedResponse -> Flux.fromIterable(cosmosItemFeedResponse.results()))
.collectList()
.block();
}
private CosmosItemResponse deleteDocument(@NonNull CosmosItemProperties cosmosItemProperties,
@NonNull List<String> partitionKeyNames,
String containerName) {
@NonNull List<String> partitionKeyNames,
String containerName,
@NonNull Class<?> domainClass) {
Assert.isTrue(partitionKeyNames.size() <= 1, "Only one Partition is supported.");
PartitionKey partitionKey = null;
@ -488,16 +501,34 @@ public class DocumentDbTemplate implements DocumentDbOperations, ApplicationCont
}
final CosmosItemRequestOptions options = new CosmosItemRequestOptions(pk);
applyVersioning(domainClass, cosmosItemProperties, options);
return cosmosClient
.getDatabase(this.databaseName)
.getContainer(containerName)
.getItem(cosmosItemProperties.id(), partitionKey)
.delete(options)
.block();
.getDatabase(this.databaseName)
.getContainer(containerName)
.getItem(cosmosItemProperties.id(), partitionKey)
.delete(options)
.block();
}
private <T> T toDomainObject(@NonNull Class<T> domainClass, CosmosItemProperties cosmosItemProperties) {
return mappingDocumentDbConverter.read(domainClass, cosmosItemProperties);
}
private void applyVersioning(Class<?> domainClass,
CosmosItemProperties cosmosItemProperties,
CosmosItemRequestOptions options) {
if (entityInfoCreator.apply(domainClass).isVersioned()) {
final AccessCondition accessCondition = new AccessCondition();
accessCondition.type(AccessConditionType.IF_MATCH);
accessCondition.condition(cosmosItemProperties.etag());
options.accessCondition(accessCondition);
}
}
private DocumentDbEntityInformation<?, ?> getDocumentDbEntityInformation(Class<?> domainClass) {
return new DocumentDbEntityInformation<>(domainClass);
}
}

Просмотреть файл

@ -17,6 +17,7 @@ import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.repository.core.support.AbstractEntityInformation;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;
@ -31,12 +32,14 @@ import java.util.List;
public class DocumentDbEntityInformation<T, ID> extends AbstractEntityInformation<T, ID> {
private static final String ETAG = "_etag";
private Field id;
private Field partitionKeyField;
private String collectionName;
private Integer requestUnit;
private Integer timeToLive;
private IndexingPolicy indexingPolicy;
private boolean isVersioned;
public DocumentDbEntityInformation(Class<T> domainClass) {
super(domainClass);
@ -53,6 +56,7 @@ public class DocumentDbEntityInformation<T, ID> extends AbstractEntityInformatio
this.requestUnit = getRequestUnit(domainClass);
this.timeToLive = getTimeToLive(domainClass);
this.indexingPolicy = getIndexingPolicy(domainClass);
this.isVersioned = getIsVersioned(domainClass);
}
@SuppressWarnings("unchecked")
@ -86,6 +90,10 @@ public class DocumentDbEntityInformation<T, ID> extends AbstractEntityInformatio
return this.indexingPolicy;
}
public boolean isVersioned() {
return isVersioned;
}
public String getPartitionKeyFieldName() {
if (partitionKeyField == null) {
return null;
@ -239,5 +247,13 @@ public class DocumentDbEntityInformation<T, ID> extends AbstractEntityInformatio
return pathsCollection;
}
private boolean getIsVersioned(Class<T> domainClass) {
final Field findField = ReflectionUtils.findField(domainClass, ETAG);
return findField != null
&& findField.getType() == String.class
&& findField.isAnnotationPresent(Version.class);
}
}

Просмотреть файл

@ -0,0 +1,57 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for
* license information.
*/
package com.microsoft.azure.spring.data.cosmosdb.common;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.IntStream;
/**
*
* @author Domenico Sibilio
*
*/
public class MemoizerUnitTest {
private static final String KEY = "key_1";
private static final Map<String, AtomicInteger> countMap = new HashMap<>();
private static final Function<String, Integer> memoizedFunction =
Memoizer.memoize(MemoizerUnitTest::incrCount);
@Before
public void setUp() {
countMap.put(KEY, new AtomicInteger(0));
}
@Test
public void testMemoizedFunctionShouldBeCalledOnlyOnce() {
IntStream
.range(0, 10)
.forEach(number -> memoizedFunction.apply(KEY));
assertEquals(1, countMap.get(KEY).get());
}
@Test
public void testDifferentMemoizersShouldNotShareTheSameCache() {
IntStream
.range(0, 10)
.forEach(number -> Memoizer.memoize(MemoizerUnitTest::incrCount).apply(KEY));
assertEquals(10, countMap.get(KEY).get());
}
private static int incrCount(String key) {
return countMap.get(key).incrementAndGet();
}
}

Просмотреть файл

@ -6,6 +6,7 @@
package com.microsoft.azure.spring.data.cosmosdb.core;
import com.azure.data.cosmos.CosmosClientException;
import com.microsoft.azure.documentdb.PartitionKey;
import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory;
import com.microsoft.azure.spring.data.cosmosdb.config.DocumentDBConfig;
@ -23,6 +24,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScanner;
@ -34,29 +36,48 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage;
import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateNonLastPage;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ADDRESSES;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.DB_NAME;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.FIRST_NAME;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.HOBBIES;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_1;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_2;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.ID_3;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.LAST_NAME;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_FIRST_NAME;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NEW_LAST_NAME;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.NOT_EXIST_ID;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_1;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_2;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.PAGE_SIZE_3;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.UPDATED_FIRST_NAME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateLastPage;
import static com.microsoft.azure.spring.data.cosmosdb.common.PageTestUtils.validateNonLastPage;
import static com.microsoft.azure.spring.data.cosmosdb.common.TestConstants.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@PropertySource(value = { "classpath:application.properties" })
public class DocumentDbTemplateIT {
private static final Person TEST_PERSON = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES,
ADDRESSES);
ADDRESSES);
private static final Person TEST_PERSON_2 = new Person(ID_2,
NEW_FIRST_NAME,
NEW_LAST_NAME, HOBBIES, ADDRESSES);
NEW_FIRST_NAME,
NEW_LAST_NAME, HOBBIES, ADDRESSES);
private static final Person TEST_PERSON_3 = new Person(ID_3,
NEW_FIRST_NAME,
NEW_LAST_NAME, HOBBIES, ADDRESSES);
NEW_FIRST_NAME,
NEW_LAST_NAME, HOBBIES, ADDRESSES);
private static final String PRECONDITION_IS_NOT_MET = "is not met";
private static final String WRONG_ETAG = "WRONG_ETAG";
@Value("${cosmosdb.uri}")
private String documentDbUri;
@ -68,6 +89,8 @@ public class DocumentDbTemplateIT {
private static String collectionName;
private static boolean initialized;
private Person insertedPerson;
@Autowired
private ApplicationContext applicationContext;
@ -76,7 +99,7 @@ public class DocumentDbTemplateIT {
public void setup() throws ClassNotFoundException {
if (!initialized) {
final DocumentDBConfig dbConfig = DocumentDBConfig.builder(documentDbUri,
documentDbKey, DB_NAME).build();
documentDbKey, DB_NAME).build();
final CosmosDbFactory cosmosDbFactory = new CosmosDbFactory(dbConfig);
final DocumentDbMappingContext mappingContext = new DocumentDbMappingContext();
@ -86,12 +109,13 @@ public class DocumentDbTemplateIT {
mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class));
final MappingDocumentDbConverter dbConverter =
new MappingDocumentDbConverter(mappingContext, null);
new MappingDocumentDbConverter(mappingContext, null);
dbTemplate = new DocumentDbTemplate(cosmosDbFactory, dbConverter, DB_NAME);
dbTemplate.createCollectionIfNotExists(personInfo);
initialized = true;
}
dbTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, null);
insertedPerson = dbTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, null);
}
@After
@ -102,7 +126,7 @@ public class DocumentDbTemplateIT {
@Test(expected = DocumentDBAccessException.class)
public void testInsertDuplicateId() {
dbTemplate.insert(Person.class.getSimpleName(), TEST_PERSON,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
}
@Test
@ -115,20 +139,20 @@ public class DocumentDbTemplateIT {
@Test
public void testFindById() {
final Person result = dbTemplate.findById(Person.class.getSimpleName(),
TEST_PERSON.getId(), Person.class);
TEST_PERSON.getId(), Person.class);
assertEquals(result, TEST_PERSON);
final Person nullResult = dbTemplate.findById(Person.class.getSimpleName(),
NOT_EXIST_ID, Person.class);
NOT_EXIST_ID, Person.class);
assertThat(nullResult).isNull();
}
@Test
public void testFindByMultiIds() {
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
dbTemplate.insert(TEST_PERSON_3,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
final List<Object> ids = Lists.newArrayList(ID_1, ID_2, ID_3);
final List<Person> result = dbTemplate.findByIds(ids, Person.class, collectionName);
@ -142,14 +166,14 @@ public class DocumentDbTemplateIT {
public void testUpsertNewDocument() {
// Delete first as was inserted in setup
dbTemplate.deleteById(Person.class.getSimpleName(), TEST_PERSON.getId(),
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
final String firstName = NEW_FIRST_NAME + "_" + UUID.randomUUID().toString();
final Person newPerson = new Person(TEST_PERSON.getId(), firstName,
NEW_FIRST_NAME, null, null);
NEW_FIRST_NAME, null, null);
dbTemplate.upsert(Person.class.getSimpleName(), newPerson,
new PartitionKey(personInfo.getPartitionKeyFieldValue(newPerson)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(newPerson)));
final List<Person> result = dbTemplate.findAll(Person.class);
@ -160,24 +184,47 @@ public class DocumentDbTemplateIT {
@Test
public void testUpdate() {
final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME,
TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(),
TEST_PERSON.getShippingAddresses());
dbTemplate.upsert(Person.class.getSimpleName(), updated,
new PartitionKey(personInfo.getPartitionKeyFieldValue(updated)));
TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses());
updated.set_etag(insertedPerson.get_etag());
dbTemplate.upsert(Person.class.getSimpleName(), updated, null);
final Person result = dbTemplate.findById(Person.class.getSimpleName(),
updated.getId(), Person.class);
updated.getId(), Person.class);
assertEquals(result, updated);
}
@Test
public void testOptimisticLockWhenUpdatingWithWrongEtag() {
final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME,
TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses());
updated.set_etag(WRONG_ETAG);
try {
dbTemplate.upsert(Person.class.getSimpleName(), updated, null);
} catch (DocumentDBAccessException e) {
assertThat(e.getCause()).isNotNull();
final Throwable cosmosClientException = e.getCause().getCause();
assertThat(cosmosClientException).isInstanceOf(CosmosClientException.class);
assertThat(cosmosClientException.getMessage()).contains(PRECONDITION_IS_NOT_MET);
final Person unmodifiedPerson = dbTemplate.findById(Person.class.getSimpleName(),
TEST_PERSON.getId(), Person.class);
assertThat(unmodifiedPerson.getFirstName()).isEqualTo(insertedPerson.getFirstName());
return;
}
fail();
}
@Test
public void testDeleteById() {
dbTemplate.insert(TEST_PERSON_2, null);
assertThat(dbTemplate.findAll(Person.class).size()).isEqualTo(2);
dbTemplate.deleteById(Person.class.getSimpleName(), TEST_PERSON.getId(),
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON)));
final List<Person> result = dbTemplate.findAll(Person.class);
assertThat(result.size()).isEqualTo(1);
@ -190,7 +237,7 @@ public class DocumentDbTemplateIT {
assertThat(prevCount).isEqualTo(1);
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
final long newCount = dbTemplate.count(collectionName);
assertThat(newCount).isEqualTo(2);
@ -199,10 +246,10 @@ public class DocumentDbTemplateIT {
@Test
public void testCountByQuery() {
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName",
Collections.singletonList(TEST_PERSON_2.getFirstName()));
Collections.singletonList(TEST_PERSON_2.getFirstName()));
final DocumentQuery query = new DocumentQuery(criteria);
final long count = dbTemplate.count(query, Person.class, collectionName);
@ -212,7 +259,7 @@ public class DocumentDbTemplateIT {
@Test
public void testFindAllPageableMultiPages() {
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
final DocumentDbPageRequest pageRequest = new DocumentDbPageRequest(0, PAGE_SIZE_1, null);
final Page<Person> page1 = dbTemplate.findAll(pageRequest, Person.class, collectionName);
@ -221,7 +268,7 @@ public class DocumentDbTemplateIT {
validateNonLastPage(page1, PAGE_SIZE_1);
final Page<Person> page2 = dbTemplate.findAll(page1.getPageable(), Person.class,
collectionName);
collectionName);
assertThat(page2.getContent().size()).isEqualTo(1);
validateLastPage(page2, PAGE_SIZE_1);
}
@ -229,10 +276,10 @@ public class DocumentDbTemplateIT {
@Test
public void testPaginationQuery() {
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName",
Collections.singletonList(FIRST_NAME));
Collections.singletonList(FIRST_NAME));
final PageRequest pageRequest = new DocumentDbPageRequest(0, PAGE_SIZE_2, null);
final DocumentQuery query = new DocumentQuery(criteria).with(pageRequest);
@ -244,9 +291,9 @@ public class DocumentDbTemplateIT {
@Test
public void testFindAllWithPageableAndSort() {
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
dbTemplate.insert(TEST_PERSON_3,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
final Sort sort = new Sort(Sort.Direction.DESC, "firstName");
final PageRequest pageRequest = DocumentDbPageRequest.of(0, PAGE_SIZE_3, null, sort);
@ -268,19 +315,19 @@ public class DocumentDbTemplateIT {
final Person testPerson5 = new Person("id_5", "fred", NEW_LAST_NAME, HOBBIES, ADDRESSES);
dbTemplate.insert(TEST_PERSON_2,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2)));
dbTemplate.insert(TEST_PERSON_3,
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)));
dbTemplate.insert(testPerson4,
new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson4)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson4)));
dbTemplate.insert(testPerson5,
new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson5)));
new PartitionKey(personInfo.getPartitionKeyFieldValue(testPerson5)));
final Sort sort = new Sort(Sort.Direction.ASC, "firstName");
final PageRequest pageRequest = DocumentDbPageRequest.of(0, PAGE_SIZE_3, null, sort);
final Page<Person> firstPage = dbTemplate.findAll(pageRequest, Person.class,
collectionName);
collectionName);
assertThat(firstPage.getContent().size()).isEqualTo(3);
validateNonLastPage(firstPage, PAGE_SIZE_3);
@ -291,7 +338,7 @@ public class DocumentDbTemplateIT {
assertThat(firstPageResults.get(2).getFirstName()).isEqualTo(testPerson5.getFirstName());
final Page<Person> secondPage = dbTemplate.findAll(firstPage.getPageable(), Person.class,
collectionName);
collectionName);
assertThat(secondPage.getContent().size()).isEqualTo(2);
validateLastPage(secondPage, PAGE_SIZE_3);

Просмотреть файл

@ -16,14 +16,15 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.util.Assert;
import static com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType.IS_EQUAL;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import static com.microsoft.azure.spring.data.cosmosdb.core.query.CriteriaType.IS_EQUAL;
@RunWith(MockitoJUnitRunner.class)
public class DocumentDbTemplateIllegalTest {
private static final String NULL_STR = null;
@ -83,9 +84,7 @@ public class DocumentDbTemplateIllegalTest {
public void findByIdIllegalArgsShouldFail() throws NoSuchMethodException {
final Method method = dbTemplateClass.getDeclaredMethod("findById", Object.class, Class.class);
checkIllegalArgument(method, null, Person.class);
checkIllegalArgument(method, EMPTY_STR, Person.class);
checkIllegalArgument(method, WHITESPACES_STR, Person.class);
checkIllegalArgument(method, DUMMY_ID, null);
}
@Test

Просмотреть файл

@ -6,16 +6,20 @@
package com.microsoft.azure.spring.data.cosmosdb.domain;
import java.util.List;
import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
import org.springframework.data.annotation.Version;
@Data
@AllArgsConstructor
@EqualsAndHashCode(exclude = "_etag")
@NoArgsConstructor
@DocumentIndexingPolicy(includePaths = TestConstants.ORDER_BY_STRING_PATH)
public class Person {
private String id;
@ -25,4 +29,14 @@ public class Person {
private String lastName;
private List<String> hobbies;
private List<Address> shippingAddresses;
@Version
private String _etag;
public Person(String id, String firstName, String lastName, List<String> hobbies, List<Address> shippingAddresses) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.hobbies = hobbies;
this.shippingAddresses = shippingAddresses;
}
}

Просмотреть файл

@ -5,16 +5,20 @@
*/
package com.microsoft.azure.spring.data.cosmosdb.repository.support;
import java.util.List;
import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey;
import com.microsoft.azure.spring.data.cosmosdb.domain.Address;
import com.microsoft.azure.spring.data.cosmosdb.domain.Person;
import com.microsoft.azure.spring.data.cosmosdb.domain.Student;
import lombok.Data;
import org.junit.Test;
import java.util.List;
import org.springframework.data.annotation.Version;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
public class DocumentDbEntityInformationUnitTest {
private static final String ID = "entity_info_test_id";
@ -54,8 +58,8 @@ public class DocumentDbEntityInformationUnitTest {
@Test
public void testCustomCollectionName() {
final DocumentDbEntityInformation<Volunteer, String> entityInformation =
new DocumentDbEntityInformation<Volunteer, String>(Volunteer.class);
final DocumentDbEntityInformation<VersionedVolunteer, String> entityInformation =
new DocumentDbEntityInformation<VersionedVolunteer, String>(VersionedVolunteer.class);
final String collectionName = entityInformation.getCollectionName();
assertThat(collectionName).isEqualTo("testCollection");
@ -87,6 +91,42 @@ public class DocumentDbEntityInformationUnitTest {
final String partitionKeyName = entityInformation.getPartitionKeyFieldName();
assertThat(partitionKeyName).isEqualTo("vol_name");
}
@Test
public void testVersionedEntity() {
final DocumentDbEntityInformation<VersionedVolunteer, String> entityInformation =
new DocumentDbEntityInformation<VersionedVolunteer, String>(VersionedVolunteer.class);
final boolean isVersioned = entityInformation.isVersioned();
assertThat(isVersioned).isTrue();
}
@Test
public void testEntityShouldNotBeVersionedWithWrongType() {
final DocumentDbEntityInformation<WrongVersionType, String> entityInformation =
new DocumentDbEntityInformation<WrongVersionType, String>(WrongVersionType.class);
final boolean isVersioned = entityInformation.isVersioned();
assertThat(isVersioned).isFalse();
}
@Test
public void testEntityShouldNotBeVersionedWithoutAnnotationOnEtag() {
final DocumentDbEntityInformation<VersionOnWrongField, String> entityInformation =
new DocumentDbEntityInformation<VersionOnWrongField, String>(VersionOnWrongField.class);
final boolean isVersioned = entityInformation.isVersioned();
assertThat(isVersioned).isFalse();
}
@Test
public void testNonVersionedEntity() {
final DocumentDbEntityInformation<Student, String> entityInformation =
new DocumentDbEntityInformation<Student, String>(Student.class);
final boolean isVersioned = entityInformation.isVersioned();
assertThat(isVersioned).isFalse();
}
@Document(collection = "testCollection")
class Volunteer {
@ -139,4 +179,30 @@ public class DocumentDbEntityInformationUnitTest {
this.name = name;
}
}
@Data
@Document(collection = "testCollection")
class VersionedVolunteer {
private String id;
private String name;
@Version
private String _etag;
}
@Data
@Document
class WrongVersionType {
private String id;
private String name;
private long _etag;
}
@Data
@Document
class VersionOnWrongField {
private String id;
@Version
private String name;
private String _etag;
}
}