Allow disabling TypeHints.

Original Pull Request #1788
Closes #1788
This commit is contained in:
Peter-Josef Meisch 2021-04-25 21:57:13 +02:00 committed by GitHub
parent 8b7f0f8327
commit 91742b1114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1435 additions and 883 deletions

View File

@ -34,8 +34,6 @@ The following annotations are available:
The most important attributes are:
** `indexName`: the name of the index to store this entity in.
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
** `type`: [line-through]#the mapping type.
If not set, the lowercased simple name of the class is used.# (deprecated since version 4.0)
** `createIndex`: flag whether to create an index on repository bootstrapping.
Default value is _true_.
See <<elasticsearch.repositories.autocreation>>
@ -170,6 +168,22 @@ public class Person {
NOTE: Type hints will not be written for nested Objects unless the properties type is `Object`, an interface or the actual value type does not match the properties declaration.
===== Disabling Type Hints
It may be necessary to disable writing of type hints when the index that should be used already exists without having the type hints defined in its mapping and with the mapping mode set to strict. In this case, writing the type hint will produce an error, as the field cannot be added automatically.
Type hints can be disabled for the whole application by overriding the method `writeTypeHints()` in a configuration class derived from `AbstractElasticsearchConfiguration` (see <<elasticsearch.clients>>).
As an alternativ they can be disabled for a single index with the `@Document` annotation:
====
[source,java]
----
@Document(indexName = "index", writeTypeHint = WriteTypeHint.FALSE)
----
====
WARNING: We strongly advise against disabling Type Hints. Only do this if you are forced to. Disabling type hints can lead to documents not being retrieved correctly from Elasticsearch in case of polymorphic data or document retrieval may fail completely.
==== Geospatial Types
Geospatial types like `Point` & `GeoPoint` are converted into _lat/lon_ pairs.

View File

@ -105,4 +105,11 @@ public @interface Document {
* Configuration of version management.
*/
VersionType versionType() default VersionType.EXTERNAL;
/**
* Defines if type hints should be written. {@see WriteTypeHint}.
*
* @since 4.3
*/
WriteTypeHint writeTypeHint() default WriteTypeHint.DEFAULT;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2021 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.annotations;
import org.springframework.data.mapping.context.MappingContext;
/**
* Defines if type hints should be written. Used by {@link Document} annotation.
*
* @author Peter-Josef Meisch
* @since 4.3
*/
public enum WriteTypeHint {
/**
* Use the global settings from the {@link MappingContext}.
*/
DEFAULT,
/**
* Always write type hints for the entity.
*/
TRUE,
/**
* Never write type hints for the entity.
*/
FALSE
}

View File

@ -26,7 +26,6 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
@ -72,6 +71,7 @@ public class ElasticsearchConfigurationSupport {
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
mappingContext.setWriteTypeHints(writeTypeHints());
return mappingContext;
}
@ -171,4 +171,17 @@ public class ElasticsearchConfigurationSupport {
protected FieldNamingStrategy fieldNamingStrategy() {
return PropertyNameFieldNamingStrategy.INSTANCE;
}
/**
* Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
* default value of {@literal true}. If you need to write to an existing index that does not have a mapping defined
* for these fields and that has a strict mapping set, then it might be necessary to disable type hints. But notice
* that in this case reading polymorphic types may fail.
*
* @return flag if type hints should be written
* @since 4.3
*/
protected boolean writeTypeHints() {
return true;
}
}

View File

@ -95,6 +95,8 @@ public class MappingBuilder {
private final ElasticsearchConverter elasticsearchConverter;
private boolean writeTypeHints = true;
public MappingBuilder(ElasticsearchConverter elasticsearchConverter) {
this.elasticsearchConverter = elasticsearchConverter;
}
@ -111,6 +113,8 @@ public class MappingBuilder {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.getMappingContext()
.getRequiredPersistentEntity(clazz);
writeTypeHints = entity.writeTypeHints();
XContentBuilder builder = jsonBuilder().startObject();
// Dynamic templates
@ -128,11 +132,14 @@ public class MappingBuilder {
}
private void writeTypeHintMapping(XContentBuilder builder) throws IOException {
builder.startObject(TYPEHINT_PROPERTY) //
.field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) //
.field(FIELD_PARAM_INDEX, false) //
.field(FIELD_PARAM_DOC_VALUES, false) //
.endObject();
if (writeTypeHints) {
builder.startObject(TYPEHINT_PROPERTY) //
.field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) //
.field(FIELD_PARAM_INDEX, false) //
.field(FIELD_PARAM_DOC_VALUES, false) //
.endObject();
}
}
private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity<?> entity,

View File

@ -17,11 +17,11 @@ 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.document.Document;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.lang.Nullable;
/**
@ -148,4 +148,16 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
*/
@Nullable
String resolveRouting(T bean);
/**
* @return the {@link FieldNamingStrategy} for the entity
* @since 4.3
*/
FieldNamingStrategy getFieldNamingStrategy();
/**
* @return true if type hints on this entity should be written.
* @since 4.3
*/
boolean writeTypeHints();
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.core.mapping;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;

View File

@ -37,6 +37,7 @@ public class SimpleElasticsearchMappingContext
private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
private boolean writeTypeHints = true;
/**
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
@ -50,6 +51,15 @@ public class SimpleElasticsearchMappingContext
this.fieldNamingStrategy = fieldNamingStrategy == null ? DEFAULT_NAMING_STRATEGY : fieldNamingStrategy;
}
/**
* Sets the flag if type hints should be written in Entities created by this instance.
*
* @since 4.3
*/
public void setWriteTypeHints(boolean writeTypeHints) {
this.writeTypeHints = writeTypeHints;
}
@Override
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType());
@ -57,12 +67,13 @@ public class SimpleElasticsearchMappingContext
@Override
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new SimpleElasticsearchPersistentEntity<>(typeInformation);
return new SimpleElasticsearchPersistentEntity<>(typeInformation,
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints));
}
@Override
protected ElasticsearchPersistentProperty createPersistentProperty(Property property,
SimpleElasticsearchPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy);
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder);
}
}

View File

@ -34,6 +34,7 @@ import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
@ -66,10 +67,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticsearchPersistentEntity.class);
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private @Nullable final Document document;
private @Nullable String indexName;
private final Lazy<SettingsParameter> settingsParameter;
@Deprecated private @Nullable String parentType;
@Deprecated private @Nullable ElasticsearchPersistentProperty parentIdProperty;
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
private @Nullable VersionType versionType;
@ -77,18 +77,21 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
private @Nullable String routing;
private final ContextConfiguration contextConfiguration;
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
public SimpleElasticsearchPersistentEntity(TypeInformation<T> typeInformation) {
public SimpleElasticsearchPersistentEntity(TypeInformation<T> typeInformation,
ContextConfiguration contextConfiguration) {
super(typeInformation);
this.contextConfiguration = contextConfiguration;
Class<T> clazz = typeInformation.getType();
org.springframework.data.elasticsearch.annotations.Document document = AnnotatedElementUtils
.findMergedAnnotation(clazz, org.springframework.data.elasticsearch.annotations.Document.class);
document = AnnotatedElementUtils.findMergedAnnotation(clazz,
org.springframework.data.elasticsearch.annotations.Document.class);
// need a Lazy here, because we need the persistent properties available
this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz));
@ -159,7 +162,31 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return createIndexAndMapping;
}
// endregion
@Override
public FieldNamingStrategy getFieldNamingStrategy() {
return contextConfiguration.getFieldNamingStrategy();
}
@Override
public boolean writeTypeHints() {
boolean writeTypeHints = contextConfiguration.writeTypeHints;
if (document != null) {
switch (document.writeTypeHint()) {
case TRUE:
writeTypeHints = true;
break;
case FALSE:
writeTypeHints = false;
break;
case DEFAULT:
break;
}
}
return writeTypeHints;
}
@Override
public void addPersistentProperty(ElasticsearchPersistentProperty property) {
@ -215,6 +242,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
* (non-Javadoc)
* @see org.springframework.data.mapping.model.BasicPersistentEntity#setPersistentPropertyAccessorFactory(org.springframework.data.mapping.model.PersistentPropertyAccessorFactory)
*/
@SuppressWarnings("SpellCheckingInspection")
@Override
public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) {
@ -327,6 +355,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
: ExpressionDependencies.none();
// noinspection ConstantConditions
return getEvaluationContext(null, expressionDependencies);
}
@ -350,6 +379,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
// noinspection ConstantConditions
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean);
@ -525,4 +555,22 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
// endregion
/**
* Configuration settings passed in from the creating {@link SimpleElasticsearchMappingContext}.
*/
static class ContextConfiguration {
private final FieldNamingStrategy fieldNamingStrategy;
private final boolean writeTypeHints;
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints) {
this.fieldNamingStrategy = fieldNamingStrategy;
this.writeTypeHints = writeTypeHints;
}
public FieldNamingStrategy getFieldNamingStrategy() {
return fieldNamingStrategy;
}
}
}

View File

@ -64,23 +64,20 @@ public class SimpleElasticsearchPersistentProperty extends
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticsearchPersistentProperty.class);
private static final List<String> SUPPORTED_ID_PROPERTY_NAMES = Arrays.asList("id", "document");
private static final PropertyNameFieldNamingStrategy DEFAULT_FIELD_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
private final boolean isId;
private final boolean isSeqNoPrimaryTerm;
private final @Nullable String annotatedFieldName;
@Nullable private ElasticsearchPersistentPropertyConverter propertyConverter;
private final boolean storeNullValue;
private final FieldNamingStrategy fieldNamingStrategy;
public SimpleElasticsearchPersistentProperty(Property property,
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder,
@Nullable FieldNamingStrategy fieldNamingStrategy) {
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
this.annotatedFieldName = getAnnotatedFieldName();
this.fieldNamingStrategy = fieldNamingStrategy == null ? PropertyNameFieldNamingStrategy.INSTANCE
: fieldNamingStrategy;
this.isId = super.isIdProperty()
|| (SUPPORTED_ID_PROPERTY_NAMES.contains(getFieldName()) && !hasExplicitFieldName());
this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType());
@ -248,6 +245,7 @@ public class SimpleElasticsearchPersistentProperty extends
public String getFieldName() {
if (annotatedFieldName == null) {
FieldNamingStrategy fieldNamingStrategy = getFieldNamingStrategy();
String fieldName = fieldNamingStrategy.getFieldName(this);
if (!StringUtils.hasText(fieldName)) {
@ -261,6 +259,16 @@ public class SimpleElasticsearchPersistentProperty extends
return annotatedFieldName;
}
private FieldNamingStrategy getFieldNamingStrategy() {
PersistentEntity<?, ElasticsearchPersistentProperty> owner = getOwner();
if (owner instanceof ElasticsearchPersistentEntity) {
return ((ElasticsearchPersistentEntity<?>) owner).getFieldNamingStrategy();
}
return DEFAULT_FIELD_NAMING_STRATEGY;
}
@Override
public boolean isIdProperty() {
return isId;

View File

@ -26,7 +26,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.json.JSONException;
@ -1176,8 +1175,74 @@ public class MappingElasticsearchConverterUnitTests {
}
}
private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getY(), point.getX());
@Test // #1454
@DisplayName("should write type hints if configured")
void shouldWriteTypeHintsIfConfigured() throws JSONException {
((SimpleElasticsearchMappingContext) mappingElasticsearchConverter.getMappingContext()).setWriteTypeHints(true);
PersonWithCars person = new PersonWithCars();
person.setId("42");
person.setName("Smith");
Car car1 = new Car();
car1.setModel("Ford Mustang");
Car car2 = new ElectricCar();
car2.setModel("Porsche Taycan");
person.setCars(Arrays.asList(car1, car2));
String expected = "{\n" + //
" \"_class\": \"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$PersonWithCars\",\n"
+ " \"id\": \"42\",\n" + //
" \"name\": \"Smith\",\n" + //
" \"cars\": [\n" + //
" {\n" + //
" \"model\": \"Ford Mustang\"\n" + //
" },\n" + //
" {\n" + //
" \"_class\": \"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$ElectricCar\",\n"
+ " \"model\": \"Porsche Taycan\"\n" + //
" }\n" + //
" ]\n" + //
"}\n"; //
Document document = Document.create();
mappingElasticsearchConverter.write(person, document);
assertEquals(expected, document.toJson(), true);
}
@Test // #1454
@DisplayName("should not write type hints if configured")
void shouldNotWriteTypeHintsIfNotConfigured() throws JSONException {
((SimpleElasticsearchMappingContext) mappingElasticsearchConverter.getMappingContext()).setWriteTypeHints(false);
PersonWithCars person = new PersonWithCars();
person.setId("42");
person.setName("Smith");
Car car1 = new Car();
car1.setModel("Ford Mustang");
Car car2 = new ElectricCar();
car2.setModel("Porsche Taycan");
person.setCars(Arrays.asList(car1, car2));
String expected = "{\n" + //
" \"id\": \"42\",\n" + //
" \"name\": \"Smith\",\n" + //
" \"cars\": [\n" + //
" {\n" + //
" \"model\": \"Ford Mustang\"\n" + //
" },\n" + //
" {\n" + //
" \"model\": \"Porsche Taycan\"\n" + //
" }\n" + //
" ]\n" + //
"}\n"; //
Document document = Document.create();
mappingElasticsearchConverter.write(person, document);
assertEquals(expected, document.toJson(), true);
}
private Map<String, Object> writeToMap(Object source) {
@ -1187,6 +1252,7 @@ public class MappingElasticsearchConverterUnitTests {
return sink;
}
// region entities
public static class Sample {
@Nullable public @ReadOnlyProperty String readOnly;
@Nullable public @Transient String annotatedTransientProperty;
@ -2008,4 +2074,39 @@ public class MappingElasticsearchConverterUnitTests {
}
}
private static class ElectricCar extends Car {}
private static class PersonWithCars {
@Id @Nullable String id;
@Field(type = FieldType.Text) @Nullable private String name;
@Field(type = FieldType.Nested) @Nullable private List<? extends Car> cars;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getName() {
return name;
}
public void setName(@Nullable String name) {
this.name = name;
}
@Nullable
public List<? extends Car> getCars() {
return cars;
}
public void setCars(@Nullable List<Car> cars) {
this.cars = cars;
}
}
// endregion
}

View File

@ -44,6 +44,7 @@ import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.completion.Completion;
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;
@ -661,6 +662,119 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
assertEquals(expected, mapping, false);
}
@Test // #1454
@DisplayName("should write type hints when context is configured to do so")
void shouldWriteTypeHintsWhenContextIsConfiguredToDoSo() throws JSONException {
((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(true);
String expected = "{\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" },\n" + //
" \"title\": {\n" + //
" \"type\": \"text\"\n" + //
" },\n" + //
" \"authors\": {\n" + //
" \"type\": \"nested\",\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}\n"; //
String mapping = getMappingBuilder().buildPropertyMapping(Magazine.class);
assertEquals(expected, mapping, true);
}
@Test // #1454
@DisplayName("should not write type hints when context is configured to not do so")
void shouldNotWriteTypeHintsWhenContextIsConfiguredToNotDoSo() throws JSONException {
((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(false);
String expected = "{\n" + //
" \"properties\": {\n" + //
" \"title\": {\n" + //
" \"type\": \"text\"\n" + //
" },\n" + //
" \"authors\": {\n" + //
" \"type\": \"nested\",\n" + //
" \"properties\": {\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}\n"; //
String mapping = getMappingBuilder().buildPropertyMapping(Magazine.class);
assertEquals(expected, mapping, true);
}
@Test // #1454
@DisplayName("should write type hints when context is configured to not do so but entity should")
void shouldWriteTypeHintsWhenContextIsConfiguredToNotDoSoButEntityShould() throws JSONException {
((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(false);
String expected = "{\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" },\n" + //
" \"title\": {\n" + //
" \"type\": \"text\"\n" + //
" },\n" + //
" \"authors\": {\n" + //
" \"type\": \"nested\",\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}\n"; //
String mapping = getMappingBuilder().buildPropertyMapping(MagazineWithTypeHints.class);
assertEquals(expected, mapping, true);
}
@Test // #1454
@DisplayName("should not write type hints when context is configured to do so but entity should not")
void shouldNotWriteTypeHintsWhenContextIsConfiguredToDoSoButEntityShouldNot() throws JSONException {
((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(true);
String expected = "{\n" + //
" \"properties\": {\n" + //
" \"title\": {\n" + //
" \"type\": \"text\"\n" + //
" },\n" + //
" \"authors\": {\n" + //
" \"type\": \"nested\",\n" + //
" \"properties\": {\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}\n"; //
String mapping = getMappingBuilder().buildPropertyMapping(MagazineWithoutTypeHints.class);
assertEquals(expected, mapping, true);
}
// region entities
@Document(indexName = "ignore-above-index")
static class IgnoreAboveEntity {
@Nullable @Id private String id;
@ -1555,4 +1669,26 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
this.field5 = field5;
}
}
@Document(indexName = "magazine")
private static class Magazine {
@Id @Nullable private String id;
@Field(type = Text) @Nullable private String title;
@Field(type = Nested) @Nullable private List<Author> authors;
}
@Document(indexName = "magazine-without-type-hints", writeTypeHint = WriteTypeHint.FALSE)
private static class MagazineWithoutTypeHints {
@Id @Nullable private String id;
@Field(type = Text) @Nullable private String title;
@Field(type = Nested) @Nullable private List<Author> authors;
}
@Document(indexName = "magazine-with-type-hints", writeTypeHint = WriteTypeHint.TRUE)
private static class MagazineWithTypeHints {
@Id @Nullable private String id;
@Field(type = Text) @Nullable private String title;
@Field(type = Nested) @Nullable private List<Author> authors;
}
// endregion
}

View File

@ -28,10 +28,14 @@ import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.annotations.WriteTypeHint;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
@ -52,13 +56,16 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@DisplayName("properties setup")
class PropertiesTests {
private final SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration(
PropertyNameFieldNamingStrategy.INSTANCE, true);
@Test
public void shouldThrowExceptionGivenVersionPropertyIsNotLong() {
TypeInformation<EntityWithWrongVersionType> typeInformation = ClassTypeInformation
.from(EntityWithWrongVersionType.class);
SimpleElasticsearchPersistentEntity<EntityWithWrongVersionType> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
assertThatThrownBy(() -> createProperty(entity, "version")).isInstanceOf(MappingException.class);
}
@ -69,7 +76,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
TypeInformation<EntityWithMultipleVersionField> typeInformation = ClassTypeInformation
.from(EntityWithMultipleVersionField.class);
SimpleElasticsearchPersistentEntity<EntityWithMultipleVersionField> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
SimpleElasticsearchPersistentProperty persistentProperty1 = createProperty(entity, "version1");
SimpleElasticsearchPersistentProperty persistentProperty2 = createProperty(entity, "version2");
entity.addPersistentProperty(persistentProperty1);
@ -98,7 +105,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
TypeInformation<EntityWithoutSeqNoPrimaryTerm> typeInformation = ClassTypeInformation
.from(EntityWithoutSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse();
}
@ -109,7 +116,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation
.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
@ -123,7 +130,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation
.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm();
SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(1, 2);
@ -142,7 +149,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = ClassTypeInformation
.from(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation);
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
assertThatThrownBy(() -> entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm2")))
@ -165,10 +172,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@DisplayName("should error if index sorting parameters do not have the same number of arguments")
void shouldErrorIfIndexSortingParametersDoNotHaveTheSameNumberOfArguments() {
assertThatThrownBy(() -> {
elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingsInvalidSortParameterSizes.class).getDefaultSettings();
}).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingsInvalidSortParameterSizes.class).getDefaultSettings())
.isInstanceOf(IllegalArgumentException.class);
}
@Test // #1719
@ -190,6 +196,75 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
}
}
@Nested
@DisplayName("configuration")
class ConfigurationTests {
@Test // #1454
@DisplayName("should return FieldNamingStrategy from context configuration")
void shouldReturnFieldNamingStrategyFromContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
FieldNamingStrategy fieldNamingStrategy = new FieldNamingStrategy() {
@Override
public String getFieldName(PersistentProperty<?> property) {
return property.getName() + "foo";
}
};
context.setFieldNamingStrategy(fieldNamingStrategy);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(FieldNameEntity.class);
assertThat(persistentEntity.getFieldNamingStrategy()).isSameAs(fieldNamingStrategy);
}
@Test // #1454
@DisplayName("should write type hints on default context settings")
void shouldWriteTypeHintsOnDefaultContextSettings() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
SimpleElasticsearchPersistentEntity<?> entity = context
.getRequiredPersistentEntity(DisableTypeHintNoSetting.class);
assertThat(entity.writeTypeHints()).isTrue();
}
@Test // #1454
@DisplayName("should not write type hints when configured in context settings")
void shouldNotWriteTypeHintsWhenConfiguredInContextSettings() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setWriteTypeHints(false);
SimpleElasticsearchPersistentEntity<?> entity = context
.getRequiredPersistentEntity(DisableTypeHintNoSetting.class);
assertThat(entity.writeTypeHints()).isFalse();
}
@Test // #1454
@DisplayName("should not write type hints when configured explicitly on entity")
void shouldNotWriteTypeHintsWhenConfiguredExplicitlyOnEntity() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
SimpleElasticsearchPersistentEntity<?> entity = context
.getRequiredPersistentEntity(DisableTypeHintExplicitSetting.class);
assertThat(entity.writeTypeHints()).isFalse();
}
@Test // #1454
@DisplayName("should write type hints when configured explicitly on entity and global setting is false")
void shouldWriteTypeHintsWhenConfiguredExplicitlyOnEntityAndGlobalSettingIsFalse() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setWriteTypeHints(false);
SimpleElasticsearchPersistentEntity<?> entity = context
.getRequiredPersistentEntity(EnableTypeHintExplicitSetting.class);
assertThat(entity.writeTypeHints()).isTrue();
}
}
// region helper functions
private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity<?> entity,
String fieldName) {
@ -198,7 +273,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
java.lang.reflect.Field field = ReflectionUtils.findField(entity.getType(), fieldName);
assertThat(field).isNotNull();
Property property = Property.of(type, field);
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT, null);
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT);
}
// endregion
@ -275,16 +350,29 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nullable @Field(name = "second-field", type = FieldType.Keyword) private String secondField;
}
@Document(indexName = "dontcare")
// property names here, not field names
@Setting(sortFields = { "secondField", "firstField" }, sortModes = { Setting.SortMode.max, Setting.SortMode.min },
sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc },
sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first })
private static class SettingsValidSortParameterSizes {
@Nullable @Id private String id;
@Nullable @Field(name = "first_field", type = FieldType.Keyword) private String firstField;
@Nullable @Field(name = "second_field", type = FieldType.Keyword) private String secondField;
}
@Document(indexName = "dontcare")
// property names here, not field names
@Setting(sortFields = { "secondField", "firstField" }, sortModes = { Setting.SortMode.max, Setting.SortMode.min },
sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc },
sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first })
private static class SettingsValidSortParameterSizes {
@Nullable @Id private String id;
@Nullable @Field(name = "first_field", type = FieldType.Keyword) private String firstField;
@Nullable @Field(name = "second_field", type = FieldType.Keyword) private String secondField;
}
private static class DisableTypeHintNoSetting {
@Nullable @Id String id;
}
@Document(indexName = "foo", writeTypeHint = WriteTypeHint.FALSE)
private static class DisableTypeHintExplicitSetting {
@Nullable @Id String id;
}
@Document(indexName = "foo", writeTypeHint = WriteTypeHint.TRUE)
private static class EnableTypeHintExplicitSetting {
@Nullable @Id String id;
}
// endregion
}

View File

@ -36,6 +36,7 @@ import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy;
import org.springframework.data.util.ClassTypeInformation;
@ -200,20 +201,22 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
@DisplayName("should use default FieldNamingStrategy")
void shouldUseDefaultFieldNamingStrategy() {
SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration(
PropertyNameFieldNamingStrategy.INSTANCE, true);
ElasticsearchPersistentEntity<FieldNamingStrategyEntity> entity = new SimpleElasticsearchPersistentEntity<>(
ClassTypeInformation.from(FieldNamingStrategyEntity.class));
ClassTypeInformation.from(FieldNamingStrategyEntity.class), contextConfiguration);
ClassTypeInformation<FieldNamingStrategyEntity> type = ClassTypeInformation.from(FieldNamingStrategyEntity.class);
java.lang.reflect.Field field = ReflectionUtils.findField(FieldNamingStrategyEntity.class,
"withoutCustomFieldName");
SimpleElasticsearchPersistentProperty property = new SimpleElasticsearchPersistentProperty(Property.of(type, field),
entity, SimpleTypeHolder.DEFAULT, null);
entity, SimpleTypeHolder.DEFAULT);
assertThat(property.getFieldName()).isEqualTo("withoutCustomFieldName");
field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withCustomFieldName");
property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT,
null);
property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT);
assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME");
}
@ -223,25 +226,27 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
void shouldUseCustomFieldNamingStrategy() {
FieldNamingStrategy fieldNamingStrategy = new SnakeCaseFieldNamingStrategy();
SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration(
fieldNamingStrategy, true);
ElasticsearchPersistentEntity<FieldNamingStrategyEntity> entity = new SimpleElasticsearchPersistentEntity<>(
ClassTypeInformation.from(FieldNamingStrategyEntity.class));
ClassTypeInformation.from(FieldNamingStrategyEntity.class), contextConfiguration);
ClassTypeInformation<FieldNamingStrategyEntity> type = ClassTypeInformation.from(FieldNamingStrategyEntity.class);
java.lang.reflect.Field field = ReflectionUtils.findField(FieldNamingStrategyEntity.class,
"withoutCustomFieldName");
SimpleElasticsearchPersistentProperty property = new SimpleElasticsearchPersistentProperty(Property.of(type, field),
entity, SimpleTypeHolder.DEFAULT, fieldNamingStrategy);
entity, SimpleTypeHolder.DEFAULT);
assertThat(property.getFieldName()).isEqualTo("without_custom_field_name");
field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withCustomFieldName");
property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT,
fieldNamingStrategy);
property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT);
assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME");
}
// region entities
static class FieldNameProperty {
@Nullable @Field(name = "by-name") String fieldProperty;
}
@ -319,4 +324,5 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
this.withCustomFieldName = withCustomFieldName;
}
}
// endregion
}