DATAES-799 - Support optimistic locking for full update scenario using seq_no + primary_term.

Original PR: #441
This commit is contained in:
Roman Puchkovskiy
2020-04-29 22:11:14 +04:00
committed by GitHub
parent 853980cdfd
commit 9b620b31bd
29 changed files with 1414 additions and 41 deletions

View File

@@ -38,6 +38,7 @@ import org.springframework.data.elasticsearch.core.document.SearchDocument;
*
* @author Mark Paluch
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
*/
public class DocumentAdaptersUnitTests {
@@ -47,7 +48,7 @@ public class DocumentAdaptersUnitTests {
Map<String, DocumentField> fields = Collections.singletonMap("field",
new DocumentField("field", Collections.singletonList("value")));
GetResult getResult = new GetResult("index", "type", "my-id", 1, 1, 42, true, null, fields, null);
GetResult getResult = new GetResult("index", "type", "my-id", 1, 2, 42, true, null, fields, null);
GetResponse response = new GetResponse(getResult);
Document document = DocumentAdapters.from(response);
@@ -57,6 +58,10 @@ public class DocumentAdaptersUnitTests {
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // DATAES-628
@@ -64,7 +69,7 @@ public class DocumentAdaptersUnitTests {
BytesArray source = new BytesArray("{\"field\":\"value\"}");
GetResult getResult = new GetResult("index", "type", "my-id", 1, 1, 42, true, source, Collections.emptyMap(), null);
GetResult getResult = new GetResult("index", "type", "my-id", 1, 2, 42, true, source, Collections.emptyMap(), null);
GetResponse response = new GetResponse(getResult);
Document document = DocumentAdapters.from(response);
@@ -74,6 +79,51 @@ public class DocumentAdaptersUnitTests {
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // DATAES-799
public void shouldAdaptGetResult() {
Map<String, DocumentField> fields = Collections.singletonMap("field",
new DocumentField("field", Collections.singletonList("value")));
GetResult getResult = new GetResult("index", "type", "my-id", 1, 2, 42, true, null, fields, null);
Document document = DocumentAdapters.from(getResult);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // DATAES-799
public void shouldAdaptGetResultSource() {
BytesArray source = new BytesArray("{\"field\":\"value\"}");
GetResult getResult = new GetResult("index", "type", "my-id", 1, 2, 42, true, source, Collections.emptyMap(), null);
Document document = DocumentAdapters.from(getResult);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // DATAES-628
@@ -83,6 +133,8 @@ public class DocumentAdaptersUnitTests {
new DocumentField("field", Collections.singletonList("value")));
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), fields);
searchHit.setSeqNo(1);
searchHit.setPrimaryTerm(2);
searchHit.score(42);
SearchDocument document = DocumentAdapters.from(searchHit);
@@ -92,6 +144,10 @@ public class DocumentAdaptersUnitTests {
assertThat(document.hasVersion()).isFalse();
assertThat(document.getScore()).isBetween(42f, 42f);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
@Test // DATAES-628
@@ -151,6 +207,8 @@ public class DocumentAdaptersUnitTests {
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), Collections.emptyMap());
searchHit.sourceRef(source).score(42);
searchHit.version(22);
searchHit.setSeqNo(1);
searchHit.setPrimaryTerm(2);
SearchDocument document = DocumentAdapters.from(searchHit);
@@ -160,5 +218,9 @@ public class DocumentAdaptersUnitTests {
assertThat(document.getVersion()).isEqualTo(22);
assertThat(document.getScore()).isBetween(42f, 42f);
assertThat(document.get("field")).isEqualTo("value");
assertThat(document.hasSeqNo()).isTrue();
assertThat(document.getSeqNo()).isEqualTo(1);
assertThat(document.hasPrimaryTerm()).isTrue();
assertThat(document.getPrimaryTerm()).isEqualTo(2);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2020 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
*
* https://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.elasticsearch.core;
import static org.assertj.core.api.Assertions.*;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.index.engine.VersionConflictEngineException;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.rest.RestStatus;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import java.util.UUID;
/**
* @author Roman Puchkovskiy
*/
class ElasticsearchExceptionTranslatorTests {
private final ElasticsearchExceptionTranslator translator = new ElasticsearchExceptionTranslator();
@Test // DATAES-799
void shouldConvertElasticsearchStatusExceptionWithSeqNoConflictToOptimisticLockingFailureException() {
ElasticsearchStatusException ex = new ElasticsearchStatusException(
"Elasticsearch exception [type=version_conflict_engine_exception, reason=[WPUUsXEB6uuA6j8_A7AB]: version conflict, required seqNo [34], primary term [16]. current document has seqNo [35] and primary term [16]]",
RestStatus.CONFLICT);
DataAccessException translated = translator.translateExceptionIfPossible(ex);
assertThat(translated).isInstanceOf(OptimisticLockingFailureException.class);
assertThat(translated.getMessage()).startsWith("Cannot index a document due to seq_no+primary_term conflict");
assertThat(translated.getCause()).isSameAs(ex);
}
@Test // DATAES-799
void shouldConvertVersionConflictEngineExceptionWithSeqNoConflictToOptimisticLockingFailureException() {
VersionConflictEngineException ex = new VersionConflictEngineException(
new ShardId("index", "uuid", 1), "exception-id",
"Elasticsearch exception [type=version_conflict_engine_exception, reason=[WPUUsXEB6uuA6j8_A7AB]: version conflict, required seqNo [34], primary term [16]. current document has seqNo [35] and primary term [16]]");
DataAccessException translated = translator.translateExceptionIfPossible(ex);
assertThat(translated).isInstanceOf(OptimisticLockingFailureException.class);
assertThat(translated.getMessage()).startsWith("Cannot index a document due to seq_no+primary_term conflict");
assertThat(translated.getCause()).isSameAs(ex);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import static java.util.Collections.*;
import static org.apache.commons.lang.RandomStringUtils.*;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
@@ -58,6 +59,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.PageRequest;
@@ -100,6 +102,7 @@ import org.springframework.lang.Nullable;
* @author Martin Choraine
* @author Farid Azaza
* @author Gyula Attila Csorogi
* @author Roman Puchkovskiy
*/
public abstract class ElasticsearchTemplateTests {
@@ -3067,6 +3070,131 @@ public abstract class ElasticsearchTemplateTests {
assertThat(operations.exists("42", index)).isTrue();
}
@Test // DATAES-799
void getShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
OptimisticEntity retrieved = operations.get(saved.getId(), OptimisticEntity.class);
assertThatSeqNoPrimaryTermIsFilled(retrieved);
}
private void assertThatSeqNoPrimaryTermIsFilled(OptimisticEntity retrieved) {
assertThat(retrieved.seqNoPrimaryTerm).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getSequenceNumber()).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getSequenceNumber()).isNotNegative();
assertThat(retrieved.seqNoPrimaryTerm.getPrimaryTerm()).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getPrimaryTerm()).isPositive();
}
@Test // DATAES-799
void multigetShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
operations.refresh(OptimisticEntity.class);
List<OptimisticEntity> retrievedList = operations.multiGet(queryForOne(saved.getId()), OptimisticEntity.class,
operations.getIndexCoordinatesFor(OptimisticEntity.class));
OptimisticEntity retrieved = retrievedList.get(0);
assertThatSeqNoPrimaryTermIsFilled(retrieved);
}
private Query queryForOne(String id) {
return new NativeSearchQueryBuilder().withIds(singletonList(id)).build();
}
@Test // DATAES-799
void searchShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
operations.refresh(OptimisticEntity.class);
SearchHits<OptimisticEntity> retrievedHits = operations.search(queryForOne(saved.getId()), OptimisticEntity.class);
OptimisticEntity retrieved = retrievedHits.getSearchHit(0).getContent();
assertThatSeqNoPrimaryTermIsFilled(retrieved);
}
@Test // DATAES-799
void multiSearchShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
operations.refresh(OptimisticEntity.class);
List<Query> queries = singletonList(queryForOne(saved.getId()));
List<SearchHits<OptimisticEntity>> retrievedHits = operations.multiSearch(queries,
OptimisticEntity.class, operations.getIndexCoordinatesFor(OptimisticEntity.class));
OptimisticEntity retrieved = retrievedHits.get(0).getSearchHit(0).getContent();
assertThatSeqNoPrimaryTermIsFilled(retrieved);
}
@Test // DATAES-799
void searchForStreamShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
operations.refresh(OptimisticEntity.class);
SearchHitsIterator<OptimisticEntity> retrievedHits = operations.searchForStream(queryForOne(saved.getId()),
OptimisticEntity.class);
OptimisticEntity retrieved = retrievedHits.next().getContent();
assertThatSeqNoPrimaryTermIsFilled(retrieved);
}
@Test // DATAES-799
void shouldThrowOptimisticLockingFailureExceptionWhenConcurrentUpdateOccursOnEntityWithSeqNoPrimaryTermProperty() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = operations.save(original);
OptimisticEntity forEdit1 = operations.get(saved.getId(), OptimisticEntity.class);
OptimisticEntity forEdit2 = operations.get(saved.getId(), OptimisticEntity.class);
forEdit1.setMessage("It'll be ok");
operations.save(forEdit1);
forEdit2.setMessage("It'll be great");
assertThatThrownBy(() -> operations.save(forEdit2))
.isInstanceOf(OptimisticLockingFailureException.class);
}
@Test // DATAES-799
void shouldThrowOptimisticLockingFailureExceptionWhenConcurrentUpdateOccursOnVersionedEntityWithSeqNoPrimaryTermProperty() {
OptimisticAndVersionedEntity original = new OptimisticAndVersionedEntity();
original.setMessage("It's fine");
OptimisticAndVersionedEntity saved = operations.save(original);
OptimisticAndVersionedEntity forEdit1 = operations.get(saved.getId(), OptimisticAndVersionedEntity.class);
OptimisticAndVersionedEntity forEdit2 = operations.get(saved.getId(), OptimisticAndVersionedEntity.class);
forEdit1.setMessage("It'll be ok");
operations.save(forEdit1);
forEdit2.setMessage("It'll be great");
assertThatThrownBy(() -> operations.save(forEdit2))
.isInstanceOf(OptimisticLockingFailureException.class);
}
@Test // DATAES-799
void shouldAllowFullReplaceOfEntityWithBothSeqNoPrimaryTermAndVersion() {
OptimisticAndVersionedEntity original = new OptimisticAndVersionedEntity();
original.setMessage("It's fine");
OptimisticAndVersionedEntity saved = operations.save(original);
OptimisticAndVersionedEntity forEdit = operations.get(saved.getId(), OptimisticAndVersionedEntity.class);
forEdit.setMessage("It'll be ok");
operations.save(forEdit);
}
protected RequestFactory getRequestFactory() {
return ((AbstractElasticsearchTemplate) operations).getRequestFactory();
}
@@ -3230,4 +3358,21 @@ public abstract class ElasticsearchTemplateTests {
@Id private String id;
private String message;
}
@Data
@Document(indexName = "test-index-optimistic-entity-template")
static class OptimisticEntity {
@Id private String id;
private String message;
private SeqNoPrimaryTerm seqNoPrimaryTerm;
}
@Data
@Document(indexName = "test-index-optimistic-and-versioned-entity-template")
static class OptimisticAndVersionedEntity {
@Id private String id;
private String message;
private SeqNoPrimaryTerm seqNoPrimaryTerm;
@Version private Long version;
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
@@ -39,6 +40,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.elasticsearch.index.query.IdsQueryBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.sort.FieldSortBuilder;
@@ -47,6 +49,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.PageRequest;
@@ -59,14 +62,7 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.Score;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.junit.junit4.ElasticsearchVersion;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.util.StringUtils;
@@ -81,6 +77,7 @@ import org.springframework.util.StringUtils;
* @author Martin Choraine
* @author Aleksei Arsenev
* @author Russell Parry
* @author Roman Puchkovskiy
*/
@SpringIntegrationTest
public class ReactiveElasticsearchTemplateTests {
@@ -855,6 +852,115 @@ public class ReactiveElasticsearchTemplateTests {
.verifyComplete();
}
@Test // DATAES-799
void getShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = template.save(original).block();
template.get(saved.getId(), OptimisticEntity.class)
.as(StepVerifier::create)
.assertNext(this::assertThatSeqNoPrimaryTermIsFilled)
.verifyComplete();
}
private void assertThatSeqNoPrimaryTermIsFilled(OptimisticEntity retrieved) {
assertThat(retrieved.seqNoPrimaryTerm).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getSequenceNumber()).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getSequenceNumber()).isNotNegative();
assertThat(retrieved.seqNoPrimaryTerm.getPrimaryTerm()).isNotNull();
assertThat(retrieved.seqNoPrimaryTerm.getPrimaryTerm()).isPositive();
}
@Test // DATAES-799
void multiGetShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = template.save(original).block();
template.multiGet(multiGetQueryForOne(saved.getId()), OptimisticEntity.class, template.getIndexCoordinatesFor(OptimisticEntity.class))
.as(StepVerifier::create)
.assertNext(this::assertThatSeqNoPrimaryTermIsFilled)
.verifyComplete();
}
private Query multiGetQueryForOne(String id) {
return new NativeSearchQueryBuilder().withIds(singletonList(id)).build();
}
@Test // DATAES-799
void searchShouldReturnSeqNoPrimaryTerm() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = template.save(original).block();
restTemplate.refresh(OptimisticEntity.class);
template.search(searchQueryForOne(saved.getId()), OptimisticEntity.class, template.getIndexCoordinatesFor(OptimisticEntity.class))
.map(SearchHit::getContent)
.as(StepVerifier::create)
.assertNext(this::assertThatSeqNoPrimaryTermIsFilled)
.verifyComplete();
}
private Query searchQueryForOne(String id) {
return new NativeSearchQueryBuilder()
.withFilter(new IdsQueryBuilder().addIds(id))
.build();
}
@Test // DATAES-799
void shouldThrowOptimisticLockingFailureExceptionWhenConcurrentUpdateOccursOnEntityWithSeqNoPrimaryTermProperty() {
OptimisticEntity original = new OptimisticEntity();
original.setMessage("It's fine");
OptimisticEntity saved = template.save(original).block();
OptimisticEntity forEdit1 = template.get(saved.getId(), OptimisticEntity.class).block();
OptimisticEntity forEdit2 = template.get(saved.getId(), OptimisticEntity.class).block();
forEdit1.setMessage("It'll be ok");
template.save(forEdit1).block();
forEdit2.setMessage("It'll be great");
template.save(forEdit2)
.as(StepVerifier::create)
.expectError(OptimisticLockingFailureException.class)
.verify();
}
@Test // DATAES-799
void shouldThrowOptimisticLockingFailureExceptionWhenConcurrentUpdateOccursOnVersionedEntityWithSeqNoPrimaryTermProperty() {
OptimisticAndVersionedEntity original = new OptimisticAndVersionedEntity();
original.setMessage("It's fine");
OptimisticAndVersionedEntity saved = template.save(original).block();
OptimisticAndVersionedEntity forEdit1 = template.get(saved.getId(), OptimisticAndVersionedEntity.class).block();
OptimisticAndVersionedEntity forEdit2 = template.get(saved.getId(), OptimisticAndVersionedEntity.class).block();
forEdit1.setMessage("It'll be ok");
template.save(forEdit1).block();
forEdit2.setMessage("It'll be great");
template.save(forEdit2)
.as(StepVerifier::create)
.expectError(OptimisticLockingFailureException.class)
.verify();
}
@Test // DATAES-799
void shouldAllowFullReplaceOfEntityWithBothSeqNoPrimaryTermAndVersion() {
OptimisticAndVersionedEntity original = new OptimisticAndVersionedEntity();
original.setMessage("It's fine");
OptimisticAndVersionedEntity saved = template.save(original).block();
OptimisticAndVersionedEntity forEdit = template.get(saved.getId(), OptimisticAndVersionedEntity.class).block();
forEdit.setMessage("It'll be ok");
template.save(forEdit)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Data
@Document(indexName = "marvel")
static class Person {
@@ -928,4 +1034,21 @@ public class ReactiveElasticsearchTemplateTests {
@Version private Long version;
@Score private float score;
}
@Data
@Document(indexName = "test-index-reactive-optimistic-entity-template")
static class OptimisticEntity {
@Id private String id;
private String message;
private SeqNoPrimaryTerm seqNoPrimaryTerm;
}
@Data
@Document(indexName = "test-index-reactive-optimistic-and-versioned-entity-template")
static class OptimisticAndVersionedEntity {
@Id private String id;
private String message;
private SeqNoPrimaryTerm seqNoPrimaryTerm;
@Version private Long version;
}
}

View File

@@ -20,8 +20,12 @@ import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.mockito.Mockito.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.Collections;
import java.util.Arrays;
import java.util.HashSet;
import org.elasticsearch.action.index.IndexAction;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.search.SearchAction;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchRequestBuilder;
@@ -44,12 +48,15 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMa
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
*/
@ExtendWith(MockitoExtension.class)
class RequestFactoryTests {
@@ -62,7 +69,7 @@ class RequestFactoryTests {
@BeforeAll
static void setUpAll() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(Collections.singleton(Person.class));
mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Person.class, EntityWithSeqNoPrimaryTerm.class)));
mappingContext.afterPropertiesSet();
converter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService());
@@ -153,9 +160,103 @@ class RequestFactoryTests {
assertThat(searchRequestBuilder.request().source().size()).isEqualTo(RequestFactory.INDEX_MAX_RESULT_WINDOW);
}
@Test // DATAES-799
void shouldIncludeSeqNoAndPrimaryTermFromIndexQueryToIndexRequest() {
IndexQuery query = new IndexQuery();
query.setObject(new Person());
query.setSeqNo(1L);
query.setPrimaryTerm(2L);
IndexRequest request = requestFactory.indexRequest(query, IndexCoordinates.of("persons"));
assertThat(request.ifSeqNo()).isEqualTo(1L);
assertThat(request.ifPrimaryTerm()).isEqualTo(2L);
}
@Test // DATAES-799
void shouldIncludeSeqNoAndPrimaryTermFromIndexQueryToIndexRequestBuilder() {
when(client.prepareIndex(anyString(), anyString()))
.thenReturn(new IndexRequestBuilder(client, IndexAction.INSTANCE));
IndexQuery query = new IndexQuery();
query.setObject(new Person());
query.setSeqNo(1L);
query.setPrimaryTerm(2L);
IndexRequestBuilder builder = requestFactory.indexRequestBuilder(client, query, IndexCoordinates.of("persons"));
assertThat(builder.request().ifSeqNo()).isEqualTo(1L);
assertThat(builder.request().ifPrimaryTerm()).isEqualTo(2L);
}
@Test // DATAES-799
void shouldNotRequestSeqNoAndPrimaryTermViaSearchRequestWhenEntityClassDoesNotContainSeqNoPrimaryTermProperty() {
Query query = new NativeSearchQueryBuilder().build();
SearchRequest request = requestFactory.searchRequest(query, Person.class, IndexCoordinates.of("persons"));
assertThat(request.source().seqNoAndPrimaryTerm()).isNull();
}
@Test // DATAES-799
void shouldRequestSeqNoAndPrimaryTermViaSearchRequestWhenEntityClassContainsSeqNoPrimaryTermProperty() {
Query query = new NativeSearchQueryBuilder().build();
SearchRequest request = requestFactory.searchRequest(query, EntityWithSeqNoPrimaryTerm.class,
IndexCoordinates.of("seqNoPrimaryTerm"));
assertThat(request.source().seqNoAndPrimaryTerm()).isTrue();
}
@Test // DATAES-799
void shouldNotRequestSeqNoAndPrimaryTermViaSearchRequestWhenEntityClassIsNull() {
Query query = new NativeSearchQueryBuilder().build();
SearchRequest request = requestFactory.searchRequest(query, null, IndexCoordinates.of("persons"));
assertThat(request.source().seqNoAndPrimaryTerm()).isNull();
}
@Test // DATAES-799
void shouldNotRequestSeqNoAndPrimaryTermViaSearchRequestBuilderWhenEntityClassDoesNotContainSeqNoPrimaryTermProperty() {
when(client.prepareSearch(any())).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE));
Query query = new NativeSearchQueryBuilder().build();
SearchRequestBuilder builder = requestFactory.searchRequestBuilder(client, query, Person.class,
IndexCoordinates.of("persons"));
assertThat(builder.request().source().seqNoAndPrimaryTerm()).isNull();
}
@Test // DATAES-799
void shouldRequestSeqNoAndPrimaryTermViaSearchRequestBuilderWhenEntityClassContainsSeqNoPrimaryTermProperty() {
when(client.prepareSearch(any())).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE));
Query query = new NativeSearchQueryBuilder().build();
SearchRequestBuilder builder = requestFactory.searchRequestBuilder(client, query,
EntityWithSeqNoPrimaryTerm.class, IndexCoordinates.of("seqNoPrimaryTerm"));
assertThat(builder.request().source().seqNoAndPrimaryTerm()).isTrue();
}
@Test // DATAES-799
void shouldNotRequestSeqNoAndPrimaryTermViaSearchRequestBuilderWhenEntityClassIsNull() {
when(client.prepareSearch(any())).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE));
Query query = new NativeSearchQueryBuilder().build();
SearchRequestBuilder builder = requestFactory.searchRequestBuilder(client, query, null,
IndexCoordinates.of("persons"));
assertThat(builder.request().source().seqNoAndPrimaryTerm()).isNull();
}
static class Person {
@Nullable @Id String id;
@Nullable @Field(name = "last-name") String lastName;
@Nullable @Field(name = "current-location") GeoPoint location;
}
static class EntityWithSeqNoPrimaryTerm {
@Nullable private SeqNoPrimaryTerm seqNoPrimaryTerm;
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core.convert;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
@@ -55,6 +56,7 @@ import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point;
@@ -69,6 +71,7 @@ import org.springframework.lang.Nullable;
* @author Mark Paluch
* @author Peter-Josef Meisch
* @author Konrad Kurdej
* @author Roman Puchkovskiy
*/
public class MappingElasticsearchConverterUnitTests {
@@ -695,6 +698,26 @@ public class MappingElasticsearchConverterUnitTests {
assertThat(wrapper.getSchemaLessObject()).isEqualTo(mapWithSimpleList);
}
@Test // DATAES-799
void shouldNotWriteSeqNoPrimaryTermProperty() {
EntityWithSeqNoPrimaryTerm entity = new EntityWithSeqNoPrimaryTerm();
entity.seqNoPrimaryTerm = new SeqNoPrimaryTerm(1L, 2L);
Document document = Document.create();
mappingElasticsearchConverter.write(entity, document);
assertThat(document).doesNotContainKey("seqNoPrimaryTerm");
}
@Test // DATAES-799
void shouldNotReadSeqNoPrimaryTermProperty() {
Document document = Document.create().append("seqNoPrimaryTerm", emptyMap());
EntityWithSeqNoPrimaryTerm entity = mappingElasticsearchConverter.read(EntityWithSeqNoPrimaryTerm.class, document);
assertThat(entity.seqNoPrimaryTerm).isNull();
}
private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY());
}
@@ -901,4 +924,11 @@ public class MappingElasticsearchConverterUnitTests {
private Map<String, Object> schemaLessObject;
}
@Data
@org.springframework.data.elasticsearch.annotations.Document(indexName = "test-index-entity-with-seq-no-primary-term-mapper")
static class EntityWithSeqNoPrimaryTerm {
@Nullable private SeqNoPrimaryTerm seqNoPrimaryTerm;
}
}

View File

@@ -20,10 +20,12 @@ import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.Object;
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -60,6 +62,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.geo.Box;
@@ -79,6 +82,7 @@ import org.springframework.test.context.ContextConfiguration;
* @author Sascha Woo
* @author Peter-Josef Meisch
* @author Xiao Yu
* @author Roman Puchkovskiy
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class })
@@ -572,6 +576,13 @@ public class MappingBuilderTests extends MappingContextBaseTests {
assertEquals(expected, mapping, false);
}
@Test // DATAES-799
void shouldNotIncludeSeqNoPrimaryTermPropertyInMappingEvenWhenAnnotatedWithField() {
String propertyMapping = getMappingBuilder().buildPropertyMapping(EntityWithSeqNoPrimaryTerm.class);
assertThat(propertyMapping).doesNotContain("seqNoPrimaryTerm");
}
/**
* @author Xiao Yu
*/
@@ -1052,4 +1063,11 @@ public class MappingBuilderTests extends MappingContextBaseTests {
@CompletionField(contexts = { @CompletionContext(name = "location", type = ContextMapping.Type.GEO,
path = "proppath") }) private Completion suggest;
}
@Data
@Document(indexName = "test-index-entity-with-seq-no-primary-term-mapping-builder")
static class EntityWithSeqNoPrimaryTerm {
@Field(type = Object) private SeqNoPrimaryTerm seqNoPrimaryTerm;
}
}

View File

@@ -22,6 +22,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.Score;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
@@ -36,6 +37,7 @@ import org.springframework.util.ReflectionUtils;
* @author Mark Paluch
* @author Oliver Gierke
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
*/
public class SimpleElasticsearchPersistentEntityTests {
@@ -95,6 +97,52 @@ public class SimpleElasticsearchPersistentEntityTests {
assertThat(persistentProperty.getFieldName()).isEqualTo("renamed-field");
}
@Test // DATAES-799
void shouldReportThatThereIsNoSeqNoPrimaryTermPropertyWhenThereIsNoSuchProperty() {
TypeInformation<EntityWithoutSeqNoPrimaryTerm> typeInformation = ClassTypeInformation.from(EntityWithoutSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse();
}
@Test // DATAES-799
void shouldReportThatThereIsSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
assertThat(entity.hasSeqNoPrimaryTermProperty()).isTrue();
}
@Test // DATAES-799
void shouldReturnSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm();
SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(1, 2);
ElasticsearchPersistentProperty property = entity.getSeqNoPrimaryTermProperty();
entity.getPropertyAccessor(instance).setProperty(property, seqNoPrimaryTerm);
assertThat(instance.seqNoPrimaryTerm).isSameAs(seqNoPrimaryTerm);
}
@Test // DATAES-799
void shouldNotAllowMoreThanOneSeqNoPrimaryTermProperties() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
assertThatThrownBy(() -> entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm2")))
.isInstanceOf(MappingException.class);
}
private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity<?> entity,
String field) {
@@ -153,4 +201,12 @@ public class SimpleElasticsearchPersistentEntityTests {
@Nullable @Id private String id;
@Nullable @Field(name = "renamed-field") private String renamedField;
}
private static class EntityWithoutSeqNoPrimaryTerm {
}
private static class EntityWithSeqNoPrimaryTerm {
private SeqNoPrimaryTerm seqNoPrimaryTerm;
private SeqNoPrimaryTerm seqNoPrimaryTerm2;
}
}

View File

@@ -23,13 +23,13 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Score;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.lang.Nullable;
@@ -38,6 +38,7 @@ import org.springframework.lang.Nullable;
*
* @author Oliver Gierke
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
*/
public class SimpleElasticsearchPersistentPropertyUnitTests {
@@ -126,7 +127,7 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
assertThat(converted).isEqualTo("20200419T194400.000Z");
}
@Test // DATES-792
@Test // DATAES-792
void shouldConvertToLegacyDate() {
SimpleElasticsearchPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class);
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("legacyDate");
@@ -140,6 +141,38 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
assertThat(converted).isEqualTo(legacyDate);
}
@Test // DATAES-799
void shouldReportSeqNoPrimaryTermPropertyWhenTheTypeIsSeqNoPrimaryTerm() {
SimpleElasticsearchPersistentEntity<?> entity = context.getRequiredPersistentEntity(SeqNoPrimaryTermProperty.class);
ElasticsearchPersistentProperty seqNoProperty = entity.getRequiredPersistentProperty("seqNoPrimaryTerm");
assertThat(seqNoProperty.isSeqNoPrimaryTermProperty()).isTrue();
}
@Test // DATAES-799
void shouldNotReportSeqNoPrimaryTermPropertyWhenTheTypeIsNotSeqNoPrimaryTerm() {
SimpleElasticsearchPersistentEntity<?> entity = context.getRequiredPersistentEntity(SeqNoPrimaryTermProperty.class);
ElasticsearchPersistentProperty stringProperty = entity.getRequiredPersistentProperty("string");
assertThat(stringProperty.isSeqNoPrimaryTermProperty()).isFalse();
}
@Test // DATAES-799
void seqNoPrimaryTermPropertyShouldNotBeWritable() {
SimpleElasticsearchPersistentEntity<?> entity = context.getRequiredPersistentEntity(SeqNoPrimaryTermProperty.class);
ElasticsearchPersistentProperty seqNoProperty = entity.getRequiredPersistentProperty("seqNoPrimaryTerm");
assertThat(seqNoProperty.isWritable()).isFalse();
}
@Test // DATAES-799
void seqNoPrimaryTermPropertyShouldNotBeReadable() {
SimpleElasticsearchPersistentEntity<?> entity = context.getRequiredPersistentEntity(SeqNoPrimaryTermProperty.class);
ElasticsearchPersistentProperty seqNoProperty = entity.getRequiredPersistentProperty("seqNoPrimaryTerm");
assertThat(seqNoProperty.isReadable()).isFalse();
}
static class InvalidScoreProperty {
@Nullable @Score String scoreProperty;
}
@@ -157,4 +190,9 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
@Nullable @Field(type = FieldType.Date, format = DateFormat.basic_date_time) LocalDateTime localDateTime;
@Nullable @Field(type = FieldType.Date, format = DateFormat.basic_date_time) Date legacyDate;
}
static class SeqNoPrimaryTermProperty {
SeqNoPrimaryTerm seqNoPrimaryTerm;
String string;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2020 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
*
* https://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.elasticsearch.core.query;
import static org.assertj.core.api.Assertions.*;
import org.elasticsearch.index.seqno.SequenceNumbers;
import org.junit.jupiter.api.Test;
/**
* @author Roman Puchkovskiy
*/
class SeqNoPrimaryTermTests {
@Test
void shouldConstructInstanceWithAssignedSeqNoAndPrimaryTerm() {
SeqNoPrimaryTerm instance = new SeqNoPrimaryTerm(1, 2);
assertThat(instance.getSequenceNumber()).isEqualTo(1);
assertThat(instance.getPrimaryTerm()).isEqualTo(2);
}
@Test
void shouldThrowAnExceptionWhenTryingToConstructWithUnassignedSeqNo() {
assertThatThrownBy(() -> new SeqNoPrimaryTerm(SequenceNumbers.UNASSIGNED_SEQ_NO, 2))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldThrowAnExceptionWhenTryingToConstructWithSeqNoForNoOpsPerformed() {
assertThatThrownBy(() -> new SeqNoPrimaryTerm(SequenceNumbers.NO_OPS_PERFORMED, 2))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldThrowAnExceptionWhenTryingToConstructWithUnassignedPrimaryTerm() {
assertThatThrownBy(() -> new SeqNoPrimaryTerm(1, SequenceNumbers.UNASSIGNED_PRIMARY_TERM))
.isInstanceOf(IllegalArgumentException.class);
}
}