mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-05-31 09:12:11 +00:00
DATAES-799 - Support optimistic locking for full update scenario using seq_no + primary_term.
Original PR: #441
This commit is contained in:
parent
853980cdfd
commit
9b620b31bd
@ -53,6 +53,7 @@ import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
|
||||
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.data.elasticsearch.support.VersionInfo;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.util.CloseableIterator;
|
||||
@ -438,6 +439,22 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private SeqNoPrimaryTerm getEntitySeqNoPrimaryTerm(Object entity) {
|
||||
ElasticsearchPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entity.getClass());
|
||||
ElasticsearchPersistentProperty property = persistentEntity.getSeqNoPrimaryTermProperty();
|
||||
|
||||
if (property != null) {
|
||||
Object seqNoPrimaryTerm = persistentEntity.getPropertyAccessor(entity).getProperty(property);
|
||||
|
||||
if (seqNoPrimaryTerm != null && SeqNoPrimaryTerm.class.isAssignableFrom(seqNoPrimaryTerm.getClass())) {
|
||||
return (SeqNoPrimaryTerm) seqNoPrimaryTerm;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private <T> IndexQuery getIndexQuery(T entity) {
|
||||
String id = getEntityId(entity);
|
||||
|
||||
@ -445,11 +462,17 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
|
||||
id = elasticsearchConverter.convertId(id);
|
||||
}
|
||||
|
||||
return new IndexQueryBuilder() //
|
||||
IndexQueryBuilder builder = new IndexQueryBuilder() //
|
||||
.withId(id) //
|
||||
.withVersion(getEntityVersion(entity)) //
|
||||
.withObject(entity) //
|
||||
.build();
|
||||
.withObject(entity);
|
||||
SeqNoPrimaryTerm seqNoPrimaryTerm = getEntitySeqNoPrimaryTerm(entity);
|
||||
if (seqNoPrimaryTerm != null) {
|
||||
builder.withSeqNoPrimaryTerm(seqNoPrimaryTerm);
|
||||
} else {
|
||||
// version cannot be used together with seq_no and primary_term
|
||||
builder.withVersion(getEntityVersion(entity));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -250,7 +250,7 @@ public interface DocumentOperations {
|
||||
* @param clazz the type of the object to be returned
|
||||
* @param index the index from which the object is read.
|
||||
* @return the found object
|
||||
* @deprecated since 4.0, use {@link #getById(String, Class, IndexCoordinates)}
|
||||
* @deprecated since 4.0, use {@link #get(String, Class, IndexCoordinates)}
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
|
@ -22,9 +22,12 @@ import java.util.List;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.ElasticsearchStatusException;
|
||||
import org.elasticsearch.common.ValidationException;
|
||||
import org.elasticsearch.index.engine.VersionConflictEngineException;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.data.elasticsearch.NoSuchIndexException;
|
||||
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
|
||||
@ -35,6 +38,7 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 3.2
|
||||
*/
|
||||
public class ElasticsearchExceptionTranslator implements PersistenceExceptionTranslator {
|
||||
@ -50,6 +54,12 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
|
||||
return new NoSuchIndexException(ObjectUtils.nullSafeToString(elasticsearchException.getMetadata("es.index")),
|
||||
ex);
|
||||
}
|
||||
|
||||
if (isSeqNoConflict(elasticsearchException)) {
|
||||
return new OptimisticLockingFailureException("Cannot index a document due to seq_no+primary_term conflict",
|
||||
elasticsearchException);
|
||||
}
|
||||
|
||||
return new UncategorizedElasticsearchException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
@ -65,6 +75,25 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isSeqNoConflict(ElasticsearchException exception) {
|
||||
|
||||
if (exception instanceof ElasticsearchStatusException) {
|
||||
ElasticsearchStatusException statusException = (ElasticsearchStatusException) exception;
|
||||
return statusException.status() == RestStatus.CONFLICT
|
||||
&& statusException.getMessage() != null
|
||||
&& statusException.getMessage().contains("type=version_conflict_engine_exception")
|
||||
&& statusException.getMessage().contains("version conflict, required seqNo");
|
||||
}
|
||||
|
||||
if (exception instanceof VersionConflictEngineException) {
|
||||
VersionConflictEngineException versionConflictEngineException = (VersionConflictEngineException) exception;
|
||||
return versionConflictEngineException.getMessage() != null
|
||||
&& versionConflictEngineException.getMessage().contains("version conflict, required seqNo");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean indexAvailable(ElasticsearchException ex) {
|
||||
|
||||
List<String> metadata = ex.getMetadata("es.index_uuid");
|
||||
|
@ -27,6 +27,7 @@ import org.elasticsearch.action.get.GetRequestBuilder;
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.elasticsearch.action.get.MultiGetRequestBuilder;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.index.IndexResponse;
|
||||
import org.elasticsearch.action.search.MultiSearchRequest;
|
||||
import org.elasticsearch.action.search.MultiSearchResponse;
|
||||
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||
@ -89,6 +90,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
private Client client;
|
||||
@Nullable private String searchTimeout;
|
||||
|
||||
private final ElasticsearchExceptionTranslator exceptionTranslator = new ElasticsearchExceptionTranslator();
|
||||
|
||||
// region Initialization
|
||||
public ElasticsearchTemplate(Client client) {
|
||||
this.client = client;
|
||||
@ -145,7 +148,14 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
maybeCallbackBeforeConvertWithQuery(query, index);
|
||||
|
||||
IndexRequestBuilder indexRequestBuilder = requestFactory.indexRequestBuilder(client, query, index);
|
||||
String documentId = indexRequestBuilder.execute().actionGet().getId();
|
||||
ActionFuture<IndexResponse> future = indexRequestBuilder.execute();
|
||||
IndexResponse response;
|
||||
try {
|
||||
response = future.actionGet();
|
||||
} catch (RuntimeException e) {
|
||||
throw translateException(e);
|
||||
}
|
||||
String documentId = response.getId();
|
||||
|
||||
// We should call this because we are not going through a mapper.
|
||||
Object queryObject = query.getObject();
|
||||
@ -360,4 +370,22 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
return client;
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* translates an Exception if possible. Exceptions that are no {@link RuntimeException}s are wrapped in a
|
||||
* RuntimeException
|
||||
*
|
||||
* @param exception the Exception to map
|
||||
* @return the potentially translated RuntimeException.
|
||||
* @since 4.0
|
||||
*/
|
||||
private RuntimeException translateException(Exception exception) {
|
||||
|
||||
RuntimeException runtimeException = exception instanceof RuntimeException ? (RuntimeException) exception
|
||||
: new RuntimeException(exception.getMessage(), exception);
|
||||
RuntimeException potentiallyTranslatedException = exceptionTranslator
|
||||
.translateExceptionIfPossible(runtimeException);
|
||||
|
||||
return potentiallyTranslatedException != null ? potentiallyTranslatedException : runtimeException;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.mapping.IdentifierAccessor;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
@ -35,6 +36,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 3.2
|
||||
*/
|
||||
class EntityOperations {
|
||||
@ -256,6 +258,21 @@ class EntityOperations {
|
||||
@Override
|
||||
@Nullable
|
||||
Number getVersion();
|
||||
|
||||
/**
|
||||
* Returns whether there is a property with type SeqNoPrimaryTerm in this entity.
|
||||
*
|
||||
* @return true if there is SeqNoPrimaryTerm property
|
||||
* @since 4.0
|
||||
*/
|
||||
boolean hasSeqNoPrimaryTerm();
|
||||
|
||||
/**
|
||||
* Returns SeqNoPropertyTerm for this entity.
|
||||
*
|
||||
* @return SeqNoPrimaryTerm, may be {@literal null}
|
||||
*/
|
||||
@Nullable SeqNoPrimaryTerm getSeqNoPrimaryTerm();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -333,6 +350,16 @@ class EntityOperations {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSeqNoPrimaryTerm() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeqNoPrimaryTerm getSeqNoPrimaryTerm() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#incrementVersion()
|
||||
@ -588,6 +615,19 @@ class EntityOperations {
|
||||
return propertyAccessor.getProperty(versionProperty, Number.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSeqNoPrimaryTerm() {
|
||||
return entity.hasSeqNoPrimaryTermProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeqNoPrimaryTerm getSeqNoPrimaryTerm() {
|
||||
|
||||
ElasticsearchPersistentProperty seqNoPrimaryTermProperty = entity.getRequiredSeqNoPrimaryTermProperty();
|
||||
|
||||
return propertyAccessor.getProperty(seqNoPrimaryTermProperty, SeqNoPrimaryTerm.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#initializeVersionProperty()
|
||||
|
@ -84,6 +84,7 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.IndexQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
|
||||
import org.springframework.data.elasticsearch.support.VersionInfo;
|
||||
@ -347,7 +348,19 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
|
||||
request.source(converter.mapObject(value).toJson(), Requests.INDEX_CONTENT_TYPE);
|
||||
|
||||
if (entity.isVersionedEntity()) {
|
||||
boolean usingSeqNo = false;
|
||||
if (entity.hasSeqNoPrimaryTerm()) {
|
||||
SeqNoPrimaryTerm seqNoPrimaryTerm = entity.getSeqNoPrimaryTerm();
|
||||
|
||||
if (seqNoPrimaryTerm != null) {
|
||||
request.setIfSeqNo(seqNoPrimaryTerm.getSequenceNumber());
|
||||
request.setIfPrimaryTerm(seqNoPrimaryTerm.getPrimaryTerm());
|
||||
usingSeqNo = true;
|
||||
}
|
||||
}
|
||||
|
||||
// seq_no and version are incompatible in the same request
|
||||
if (!usingSeqNo && entity.isVersionedEntity()) {
|
||||
|
||||
Number version = entity.getVersion();
|
||||
|
||||
@ -356,6 +369,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
request.versionType(EXTERNAL);
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Sascha Woo
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 4.0
|
||||
*/
|
||||
class RequestFactory {
|
||||
@ -342,6 +343,13 @@ class RequestFactory {
|
||||
indexRequest.versionType(versionType);
|
||||
}
|
||||
|
||||
if (query.getSeqNo() != null) {
|
||||
indexRequest.setIfSeqNo(query.getSeqNo());
|
||||
}
|
||||
if (query.getPrimaryTerm() != null) {
|
||||
indexRequest.setIfPrimaryTerm(query.getPrimaryTerm());
|
||||
}
|
||||
|
||||
return indexRequest;
|
||||
}
|
||||
|
||||
@ -374,6 +382,13 @@ class RequestFactory {
|
||||
indexRequestBuilder.setVersionType(versionType);
|
||||
}
|
||||
|
||||
if (query.getSeqNo() != null) {
|
||||
indexRequestBuilder.setIfSeqNo(query.getSeqNo());
|
||||
}
|
||||
if (query.getPrimaryTerm() != null) {
|
||||
indexRequestBuilder.setIfPrimaryTerm(query.getPrimaryTerm());
|
||||
}
|
||||
|
||||
return indexRequestBuilder;
|
||||
}
|
||||
|
||||
@ -618,6 +633,9 @@ class RequestFactory {
|
||||
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
|
||||
sourceBuilder.version(true);
|
||||
sourceBuilder.trackScores(query.getTrackScores());
|
||||
if (hasSeqNoPrimaryTermProperty(clazz)) {
|
||||
sourceBuilder.seqNoAndPrimaryTerm(true);
|
||||
}
|
||||
|
||||
if (query.getSourceFilter() != null) {
|
||||
SourceFilter sourceFilter = query.getSourceFilter();
|
||||
@ -681,7 +699,20 @@ class RequestFactory {
|
||||
return request;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean hasSeqNoPrimaryTermProperty(@Nullable Class<?> entityClass) {
|
||||
|
||||
if (entityClass == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!elasticsearchConverter.getMappingContext().hasPersistentEntityFor(entityClass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(entityClass);
|
||||
return entity.hasSeqNoPrimaryTermProperty();
|
||||
}
|
||||
|
||||
public PutMappingRequest putMappingRequest(IndexCoordinates index, Document mapping) {
|
||||
PutMappingRequest request = new PutMappingRequest(index.getIndexName());
|
||||
request.source(mapping);
|
||||
@ -784,6 +815,9 @@ class RequestFactory {
|
||||
.setSearchType(query.getSearchType()) //
|
||||
.setVersion(true) //
|
||||
.setTrackScores(query.getTrackScores());
|
||||
if (hasSeqNoPrimaryTermProperty(clazz)) {
|
||||
searchRequestBuilder.seqNoAndPrimaryTerm(true);
|
||||
}
|
||||
|
||||
if (query.getSourceFilter() != null) {
|
||||
SourceFilter sourceFilter = query.getSourceFilter();
|
||||
|
@ -15,17 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@ -44,6 +35,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter;
|
||||
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
@ -205,6 +197,14 @@ public class MappingElasticsearchConverter
|
||||
targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) {
|
||||
if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) {
|
||||
SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm());
|
||||
ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty();
|
||||
targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source instanceof SearchDocument) {
|
||||
@ -220,6 +220,14 @@ public class MappingElasticsearchConverter
|
||||
|
||||
}
|
||||
|
||||
private boolean isAssignedSeqNo(long seqNo) {
|
||||
return seqNo >= 0;
|
||||
}
|
||||
|
||||
private boolean isAssignedPrimaryTerm(long primaryTerm) {
|
||||
return primaryTerm > 0;
|
||||
}
|
||||
|
||||
protected <R> R readProperties(ElasticsearchPersistentEntity<?> entity, R instance,
|
||||
ElasticsearchPropertyValueProvider valueProvider) {
|
||||
|
||||
@ -228,7 +236,7 @@ public class MappingElasticsearchConverter
|
||||
|
||||
for (ElasticsearchPersistentProperty prop : entity) {
|
||||
|
||||
if (entity.isConstructorArgument(prop) || prop.isScoreProperty()) {
|
||||
if (entity.isConstructorArgument(prop) || prop.isScoreProperty() || !prop.isReadable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface Document extends Map<String, Object> {
|
||||
@ -165,6 +166,70 @@ public interface Document extends Map<String, Object> {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@literal true} if this {@link Document} is associated with a seq_no.
|
||||
*
|
||||
* @return {@literal true} if this {@link Document} is associated with a seq_no, {@literal false} otherwise.
|
||||
*/
|
||||
default boolean hasSeqNo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the seq_no associated with this {@link Document}.
|
||||
* <p>
|
||||
* The default implementation throws {@link UnsupportedOperationException}. It's recommended to check
|
||||
* {@link #hasSeqNo()} prior to calling this method.
|
||||
*
|
||||
* @return the seq_no associated with this {@link Document}.
|
||||
* @throws IllegalStateException if the underlying implementation supports seq_no's but no seq_no was yet
|
||||
* associated with the document.
|
||||
*/
|
||||
default long getSeqNo() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the seq_no for this {@link Document}.
|
||||
* <p>
|
||||
* The default implementation throws {@link UnsupportedOperationException}.
|
||||
*/
|
||||
default void setSeqNo(long seqNo) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@literal true} if this {@link Document} is associated with a primary_term.
|
||||
*
|
||||
* @return {@literal true} if this {@link Document} is associated with a primary_term, {@literal false} otherwise.
|
||||
*/
|
||||
default boolean hasPrimaryTerm() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the primary_term associated with this {@link Document}.
|
||||
* <p>
|
||||
* The default implementation throws {@link UnsupportedOperationException}. It's recommended to check
|
||||
* {@link #hasPrimaryTerm()} prior to calling this method.
|
||||
*
|
||||
* @return the primary_term associated with this {@link Document}.
|
||||
* @throws IllegalStateException if the underlying implementation supports primary_term's but no primary_term was
|
||||
* yet associated with the document.
|
||||
*/
|
||||
default long getPrimaryTerm() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary_term for this {@link Document}.
|
||||
* <p>
|
||||
* The default implementation throws {@link UnsupportedOperationException}.
|
||||
*/
|
||||
default void setPrimaryTerm(long primaryTerm) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
|
||||
* mapping for the key. The value is casted within the method which makes it useful for calling code as it does not
|
||||
|
@ -54,6 +54,7 @@ import com.fasterxml.jackson.core.JsonGenerator;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 4.0
|
||||
*/
|
||||
public class DocumentAdapters {
|
||||
@ -76,12 +77,15 @@ public class DocumentAdapters {
|
||||
}
|
||||
|
||||
if (source.isSourceEmpty()) {
|
||||
return fromDocumentFields(source, source.getId(), source.getVersion());
|
||||
return fromDocumentFields(source, source.getId(), source.getVersion(),
|
||||
source.getSeqNo(), source.getPrimaryTerm());
|
||||
}
|
||||
|
||||
Document document = Document.from(source.getSourceAsMap());
|
||||
document.setId(source.getId());
|
||||
document.setVersion(source.getVersion());
|
||||
document.setSeqNo(source.getSeqNo());
|
||||
document.setPrimaryTerm(source.getPrimaryTerm());
|
||||
|
||||
return document;
|
||||
}
|
||||
@ -104,12 +108,15 @@ public class DocumentAdapters {
|
||||
}
|
||||
|
||||
if (source.isSourceEmpty()) {
|
||||
return fromDocumentFields(source, source.getId(), source.getVersion());
|
||||
return fromDocumentFields(source, source.getId(), source.getVersion(),
|
||||
source.getSeqNo(), source.getPrimaryTerm());
|
||||
}
|
||||
|
||||
Document document = Document.from(source.getSource());
|
||||
document.setId(source.getId());
|
||||
document.setVersion(source.getVersion());
|
||||
document.setSeqNo(source.getSeqNo());
|
||||
document.setPrimaryTerm(source.getPrimaryTerm());
|
||||
|
||||
return document;
|
||||
}
|
||||
@ -150,7 +157,8 @@ public class DocumentAdapters {
|
||||
|
||||
if (sourceRef == null || sourceRef.length() == 0) {
|
||||
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
|
||||
fromDocumentFields(source, source.getId(), source.getVersion()));
|
||||
fromDocumentFields(source, source.getId(), source.getVersion(),
|
||||
source.getSeqNo(), source.getPrimaryTerm()));
|
||||
}
|
||||
|
||||
Document document = Document.from(source.getSourceAsMap());
|
||||
@ -159,6 +167,8 @@ public class DocumentAdapters {
|
||||
if (source.getVersion() >= 0) {
|
||||
document.setVersion(source.getVersion());
|
||||
}
|
||||
document.setSeqNo(source.getSeqNo());
|
||||
document.setPrimaryTerm(source.getPrimaryTerm());
|
||||
|
||||
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
|
||||
document);
|
||||
@ -170,10 +180,11 @@ public class DocumentAdapters {
|
||||
* @param documentFields the {@link DocumentField}s backing the {@link Document}.
|
||||
* @return the adapted {@link Document}.
|
||||
*/
|
||||
public static Document fromDocumentFields(Iterable<DocumentField> documentFields, String id, long version) {
|
||||
public static Document fromDocumentFields(Iterable<DocumentField> documentFields, String id, long version,
|
||||
long seqNo, long primaryTerm) {
|
||||
|
||||
if (documentFields instanceof Collection) {
|
||||
return new DocumentFieldAdapter((Collection<DocumentField>) documentFields, id, version);
|
||||
return new DocumentFieldAdapter((Collection<DocumentField>) documentFields, id, version, seqNo, primaryTerm);
|
||||
}
|
||||
|
||||
List<DocumentField> fields = new ArrayList<>();
|
||||
@ -181,7 +192,7 @@ public class DocumentAdapters {
|
||||
fields.add(documentField);
|
||||
}
|
||||
|
||||
return new DocumentFieldAdapter(fields, id, version);
|
||||
return new DocumentFieldAdapter(fields, id, version, seqNo, primaryTerm);
|
||||
}
|
||||
|
||||
// TODO: Performance regarding keys/values/entry-set
|
||||
@ -190,11 +201,16 @@ public class DocumentAdapters {
|
||||
private final Collection<DocumentField> documentFields;
|
||||
private final String id;
|
||||
private final long version;
|
||||
private final long seqNo;
|
||||
private final long primaryTerm;
|
||||
|
||||
DocumentFieldAdapter(Collection<DocumentField> documentFields, String id, long version) {
|
||||
DocumentFieldAdapter(Collection<DocumentField> documentFields, String id, long version,
|
||||
long seqNo, long primaryTerm) {
|
||||
this.documentFields = documentFields;
|
||||
this.id = id;
|
||||
this.version = version;
|
||||
this.seqNo = seqNo;
|
||||
this.primaryTerm = primaryTerm;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -238,6 +254,52 @@ public class DocumentAdapters {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasSeqNo() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public long getSeqNo() {
|
||||
|
||||
if (!hasSeqNo()) {
|
||||
throw new IllegalStateException("No seq_no associated with this Document");
|
||||
}
|
||||
|
||||
return this.seqNo;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasPrimaryTerm() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public long getPrimaryTerm() {
|
||||
|
||||
if (!hasPrimaryTerm()) {
|
||||
throw new IllegalStateException("No primary_term associated with this Document");
|
||||
}
|
||||
|
||||
return this.primaryTerm;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.Map#size()
|
||||
@ -556,6 +618,60 @@ public class DocumentAdapters {
|
||||
public void setVersion(long version) {
|
||||
delegate.setVersion(version);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasSeqNo() {
|
||||
return delegate.hasSeqNo();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public long getSeqNo() {
|
||||
return delegate.getSeqNo();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#setSeqNo(long)
|
||||
*/
|
||||
@Override
|
||||
public void setSeqNo(long seqNo) {
|
||||
delegate.setSeqNo(seqNo);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasPrimaryTerm() {
|
||||
return delegate.hasPrimaryTerm();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public long getPrimaryTerm() {
|
||||
return delegate.getPrimaryTerm();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#setPrimaryTerm(long)
|
||||
*/
|
||||
@Override
|
||||
public void setPrimaryTerm(long primaryTerm) {
|
||||
delegate.setPrimaryTerm(primaryTerm);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
|
@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
* {@link Document} implementation backed by a {@link LinkedHashMap}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 4.0
|
||||
*/
|
||||
class MapDocument implements Document {
|
||||
@ -41,6 +42,8 @@ class MapDocument implements Document {
|
||||
|
||||
private @Nullable String id;
|
||||
private @Nullable Long version;
|
||||
private @Nullable Long seqNo;
|
||||
private @Nullable Long primaryTerm;
|
||||
|
||||
MapDocument() {
|
||||
this(new LinkedHashMap<>());
|
||||
@ -114,6 +117,68 @@ class MapDocument implements Document {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasSeqNo() {
|
||||
return this.seqNo != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getSeqNo()
|
||||
*/
|
||||
@Override
|
||||
public long getSeqNo() {
|
||||
|
||||
if (!hasSeqNo()) {
|
||||
throw new IllegalStateException("No seq_no associated with this Document");
|
||||
}
|
||||
|
||||
return this.seqNo;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#setSeqNo()
|
||||
*/
|
||||
public void setSeqNo(long seqNo) {
|
||||
this.seqNo = seqNo;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#hasPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public boolean hasPrimaryTerm() {
|
||||
return this.primaryTerm != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#getPrimaryTerm()
|
||||
*/
|
||||
@Override
|
||||
public long getPrimaryTerm() {
|
||||
|
||||
if (!hasPrimaryTerm()) {
|
||||
throw new IllegalStateException("No primary_term associated with this Document");
|
||||
}
|
||||
|
||||
return this.primaryTerm;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.document.Document#setPrimaryTerm()
|
||||
*/
|
||||
public void setPrimaryTerm(long primaryTerm) {
|
||||
this.primaryTerm = primaryTerm;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.Map#size()
|
||||
|
@ -167,6 +167,15 @@ public class MappingBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.isSeqNoPrimaryTermProperty()) {
|
||||
if (property.isAnnotationPresent(Field.class)) {
|
||||
logger.warn("Property {} of {} is annotated for inclusion in mapping, but its type is " + //
|
||||
"SeqNoPrimaryTerm that is never mapped, so it is skipped", //
|
||||
property.getFieldName(), entity.getType());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buildPropertyMapping(builder, isRootObject, property);
|
||||
} catch (IOException e) {
|
||||
logger.warn("error mapping property with name {}", property.getName(), e);
|
||||
|
@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core.mapping;
|
||||
|
||||
import org.elasticsearch.index.VersionType;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@ -30,6 +31,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Oliver Gierke
|
||||
* @author Ivan Greene
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, ElasticsearchPersistentProperty> {
|
||||
|
||||
@ -96,4 +98,41 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
|
||||
*/
|
||||
@Nullable
|
||||
ElasticsearchPersistentProperty getPersistentPropertyWithFieldName(String fieldName);
|
||||
|
||||
/**
|
||||
* Returns whether the {@link ElasticsearchPersistentEntity} has a {@link SeqNoPrimaryTerm} property. If this call
|
||||
* returns {@literal true}, {@link #getSeqNoPrimaryTermProperty()} will return a non-{@literal null} value.
|
||||
*
|
||||
* @return false when {@link ElasticsearchPersistentEntity} does not define a SeqNoPrimaryTerm property.
|
||||
* @since 4.0
|
||||
*/
|
||||
boolean hasSeqNoPrimaryTermProperty();
|
||||
|
||||
/**
|
||||
* Returns the {@link SeqNoPrimaryTerm} property of the {@link ElasticsearchPersistentEntity}. Can be
|
||||
* {@literal null} in case no such property is available on the entity.
|
||||
*
|
||||
* @return the {@link SeqNoPrimaryTerm} {@link ElasticsearchPersistentProperty} of the {@link PersistentEntity} or
|
||||
* {@literal null} if not defined.
|
||||
* @since 4.0
|
||||
*/
|
||||
@Nullable
|
||||
ElasticsearchPersistentProperty getSeqNoPrimaryTermProperty();
|
||||
|
||||
/**
|
||||
* Returns the {@link SeqNoPrimaryTerm} property of the {@link ElasticsearchPersistentEntity} or throws an
|
||||
* IllegalStateException in case no such property is available on the entity.
|
||||
*
|
||||
* @return the {@link SeqNoPrimaryTerm} {@link ElasticsearchPersistentProperty} of the {@link PersistentEntity}.
|
||||
* @since 4.0
|
||||
*/
|
||||
default ElasticsearchPersistentProperty getRequiredSeqNoPrimaryTermProperty() {
|
||||
ElasticsearchPersistentProperty property = this.getSeqNoPrimaryTermProperty();
|
||||
if (property != null) {
|
||||
return property;
|
||||
} else {
|
||||
throw new IllegalStateException(String.format("Required SeqNoPrimaryTerm property not found for %s!",
|
||||
this.getType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Sascha Woo
|
||||
* @author Oliver Gierke
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
public interface ElasticsearchPersistentProperty extends PersistentProperty<ElasticsearchPersistentProperty> {
|
||||
|
||||
@ -64,6 +65,14 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
|
||||
*/
|
||||
boolean isParentProperty();
|
||||
|
||||
/**
|
||||
* Returns whether the current property is a {@link SeqNoPrimaryTerm} property.
|
||||
*
|
||||
* @return true if the type is {@link SeqNoPrimaryTerm}
|
||||
* @since 4.0
|
||||
*/
|
||||
boolean isSeqNoPrimaryTermProperty();
|
||||
|
||||
/**
|
||||
* @return true if an {@link ElasticsearchPersistentPropertyConverter} is available for this instance.
|
||||
* @since 4.0
|
||||
@ -77,6 +86,14 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
|
||||
@Nullable
|
||||
ElasticsearchPersistentPropertyConverter getPropertyConverter();
|
||||
|
||||
/**
|
||||
* Returns true if the property may be read.
|
||||
*
|
||||
* @return true if readable, false otherwise
|
||||
* @since 4.0
|
||||
*/
|
||||
boolean isReadable();
|
||||
|
||||
enum PropertyToFieldNameConverter implements Converter<ElasticsearchPersistentProperty, String> {
|
||||
|
||||
INSTANCE;
|
||||
|
@ -23,6 +23,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.elasticsearch.index.VersionType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@ -53,10 +55,13 @@ import org.springframework.util.Assert;
|
||||
* @author Sascha Woo
|
||||
* @author Ivan Greene
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntity<T, ElasticsearchPersistentProperty>
|
||||
implements ElasticsearchPersistentEntity<T>, ApplicationContextAware {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticsearchPersistentEntity.class);
|
||||
|
||||
private final StandardEvaluationContext context;
|
||||
private final SpelExpressionParser parser;
|
||||
|
||||
@ -70,6 +75,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
private @Nullable String parentType;
|
||||
private @Nullable ElasticsearchPersistentProperty parentIdProperty;
|
||||
private @Nullable ElasticsearchPersistentProperty scoreProperty;
|
||||
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
|
||||
private @Nullable String settingPath;
|
||||
private @Nullable VersionType versionType;
|
||||
private boolean createIndexAndMapping;
|
||||
@ -231,6 +237,36 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
this.scoreProperty = property;
|
||||
}
|
||||
|
||||
if (property.isSeqNoPrimaryTermProperty()) {
|
||||
|
||||
ElasticsearchPersistentProperty seqNoPrimaryTermProperty = this.seqNoPrimaryTermProperty;
|
||||
|
||||
if (seqNoPrimaryTermProperty != null) {
|
||||
throw new MappingException(String.format(
|
||||
"Attempt to add SeqNoPrimaryTerm property %s but already have property %s registered "
|
||||
+ "as SeqNoPrimaryTerm property. Check your entity configuration!",
|
||||
property.getField(), seqNoPrimaryTermProperty.getField()));
|
||||
}
|
||||
|
||||
this.seqNoPrimaryTermProperty = property;
|
||||
|
||||
if (hasVersionProperty()) {
|
||||
warnAboutBothSeqNoPrimaryTermAndVersionProperties();
|
||||
}
|
||||
}
|
||||
|
||||
if (property.isVersionProperty()) {
|
||||
if (hasSeqNoPrimaryTermProperty()) {
|
||||
warnAboutBothSeqNoPrimaryTermAndVersionProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void warnAboutBothSeqNoPrimaryTermAndVersionProperties() {
|
||||
LOGGER.warn(
|
||||
"Both SeqNoPrimaryTerm and @Version properties are defined on {}. Version will not be sent in index requests when seq_no is sent!",
|
||||
getType());
|
||||
}
|
||||
|
||||
/*
|
||||
@ -262,4 +298,15 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return propertyRef.get();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSeqNoPrimaryTermProperty() {
|
||||
return seqNoPrimaryTermProperty != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ElasticsearchPersistentProperty getSeqNoPrimaryTermProperty() {
|
||||
return seqNoPrimaryTermProperty;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.Parent;
|
||||
import org.springframework.data.elasticsearch.annotations.Score;
|
||||
import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
@ -44,6 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Sascha Woo
|
||||
* @author Oliver Gierke
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
public class SimpleElasticsearchPersistentProperty extends
|
||||
AnnotationBasedPersistentProperty<ElasticsearchPersistentProperty> implements ElasticsearchPersistentProperty {
|
||||
@ -53,6 +55,7 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
private final boolean isScore;
|
||||
private final boolean isParent;
|
||||
private final boolean isId;
|
||||
private final boolean isSeqNoPrimaryTerm;
|
||||
private final @Nullable String annotatedFieldName;
|
||||
@Nullable private ElasticsearchPersistentPropertyConverter propertyConverter;
|
||||
|
||||
@ -65,6 +68,7 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
this.isId = super.isIdProperty() || SUPPORTED_ID_PROPERTY_NAMES.contains(getFieldName());
|
||||
this.isScore = isAnnotationPresent(Score.class);
|
||||
this.isParent = isAnnotationPresent(Parent.class);
|
||||
this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType());
|
||||
|
||||
if (isVersionProperty() && !getType().equals(Long.class)) {
|
||||
throw new MappingException(String.format("Version property %s must be of type Long!", property.getName()));
|
||||
@ -93,6 +97,16 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
return propertyConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return super.isWritable() && !isSeqNoPrimaryTermProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return !isTransient() && !isSeqNoPrimaryTermProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an {@link ElasticsearchPersistentPropertyConverter} if this property is annotated as a Field with type
|
||||
* {@link FieldType#Date}, has a {@link DateFormat} set and if the type of the property is one of the Java8 temporal
|
||||
@ -209,4 +223,13 @@ public class SimpleElasticsearchPersistentProperty extends
|
||||
public boolean isParentProperty() {
|
||||
return isParent;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty#isSeqNoPrimaryTermProperty()
|
||||
*/
|
||||
@Override
|
||||
public boolean isSeqNoPrimaryTermProperty() {
|
||||
return isSeqNoPrimaryTerm;
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ import org.springframework.lang.Nullable;
|
||||
* @author Rizwan Idrees
|
||||
* @author Mohsin Husen
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
|
||||
public class IndexQuery {
|
||||
|
||||
@Nullable private String id;
|
||||
@ -32,6 +32,8 @@ public class IndexQuery {
|
||||
@Nullable private Long version;
|
||||
@Nullable private String source;
|
||||
@Nullable private String parentId;
|
||||
@Nullable private Long seqNo;
|
||||
@Nullable private Long primaryTerm;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
@ -87,4 +89,22 @@ public class IndexQuery {
|
||||
public void setParentId(String parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getSeqNo() {
|
||||
return seqNo;
|
||||
}
|
||||
|
||||
public void setSeqNo(Long seqNo) {
|
||||
this.seqNo = seqNo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getPrimaryTerm() {
|
||||
return primaryTerm;
|
||||
}
|
||||
|
||||
public void setPrimaryTerm(Long primaryTerm) {
|
||||
this.primaryTerm = primaryTerm;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Rizwan Idrees
|
||||
* @author Mohsin Husen
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
*/
|
||||
public class IndexQueryBuilder {
|
||||
|
||||
@ -31,6 +32,8 @@ public class IndexQueryBuilder {
|
||||
@Nullable private Long version;
|
||||
@Nullable private String source;
|
||||
@Nullable private String parentId;
|
||||
@Nullable private Long seqNo;
|
||||
@Nullable private Long primaryTerm;
|
||||
|
||||
public IndexQueryBuilder withId(String id) {
|
||||
this.id = id;
|
||||
@ -57,6 +60,12 @@ public class IndexQueryBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public IndexQueryBuilder withSeqNoPrimaryTerm(SeqNoPrimaryTerm seqNoPrimaryTerm) {
|
||||
this.seqNo = seqNoPrimaryTerm.getSequenceNumber();
|
||||
this.primaryTerm = seqNoPrimaryTerm.getPrimaryTerm();
|
||||
return this;
|
||||
}
|
||||
|
||||
public IndexQuery build() {
|
||||
IndexQuery indexQuery = new IndexQuery();
|
||||
indexQuery.setId(id);
|
||||
@ -64,6 +73,8 @@ public class IndexQueryBuilder {
|
||||
indexQuery.setParentId(parentId);
|
||||
indexQuery.setSource(source);
|
||||
indexQuery.setVersion(version);
|
||||
indexQuery.setSeqNo(seqNo);
|
||||
indexQuery.setPrimaryTerm(primaryTerm);
|
||||
return indexQuery;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 java.util.Objects;
|
||||
|
||||
/**
|
||||
* <p>A container for seq_no and primary_term values. When an entity class contains a field of this type,
|
||||
* it will be automatically filled with SeqNoPrimaryTerm instance on read operations (like get or search),
|
||||
* and also, when the SeqNoPrimaryTerm is not {@literal null} and filled with seq_no and primary_term,
|
||||
* they will be sent to Elasticsearch when indexing such an entity.
|
||||
* </p>
|
||||
* <p>This allows to implement optimistic locking pattern for full-update scenario, when an entity is first
|
||||
* read from Elasticsearch and then gets reindexed with new _content.
|
||||
* Index operations will throw an {@link org.springframework.dao.OptimisticLockingFailureException} if the
|
||||
* seq_no + primary_term pair already has different values for the given document. See Elasticsearch documentation
|
||||
* for more information: https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html
|
||||
* </p>
|
||||
* <p>A property of this type is implicitly @{@link org.springframework.data.annotation.Transient} and never gets included
|
||||
* into a mapping at Elasticsearch side.
|
||||
* </p>
|
||||
* <p>A SeqNoPrimaryTerm instance cannot contain an invalid or unassigned seq_no or primary_term.
|
||||
* </p>
|
||||
*
|
||||
* @author Roman Puchkovskiy
|
||||
* @since 4.0
|
||||
*/
|
||||
public final class SeqNoPrimaryTerm {
|
||||
private final long sequenceNumber;
|
||||
private final long primaryTerm;
|
||||
|
||||
/**
|
||||
* Creates an instance of SeqNoPrimaryTerm with the given seq_no and primary_term. The passed values are validated:
|
||||
* sequenceNumber must be non-negative, primaryTerm must be positive. If validation fails,
|
||||
* an IllegalArgumentException is thrown.
|
||||
*
|
||||
* @param sequenceNumber seq_no, must not be negative
|
||||
* @param primaryTerm primary_term, must be positive
|
||||
* @throws IllegalArgumentException if seq_no or primary_term is not valid
|
||||
*/
|
||||
public SeqNoPrimaryTerm(long sequenceNumber, long primaryTerm) {
|
||||
if (sequenceNumber < 0) {
|
||||
throw new IllegalArgumentException("seq_no should not be negative, but it's " + sequenceNumber);
|
||||
}
|
||||
if (primaryTerm <= 0) {
|
||||
throw new IllegalArgumentException("primary_term should be positive, but it's " + primaryTerm);
|
||||
}
|
||||
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.primaryTerm = primaryTerm;
|
||||
}
|
||||
|
||||
public long getSequenceNumber() {
|
||||
return sequenceNumber;
|
||||
}
|
||||
|
||||
public long getPrimaryTerm() {
|
||||
return primaryTerm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SeqNoPrimaryTerm{" +
|
||||
"sequenceNumber=" + sequenceNumber +
|
||||
", primaryTerm=" + primaryTerm +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
SeqNoPrimaryTerm that = (SeqNoPrimaryTerm) o;
|
||||
return sequenceNumber == that.sequenceNumber &&
|
||||
primaryTerm == that.primaryTerm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sequenceNumber, primaryTerm);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user