Use FieldNamingStrategy for property name matching.

Original PR #1648 
Closes #1565
This commit is contained in:
Peter-Josef Meisch 2021-01-15 19:39:19 +01:00 committed by GitHub
parent ee30ef6635
commit e311df98c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 354 additions and 7 deletions

View File

@ -71,6 +71,13 @@ This is due to a https://www.elastic.co/guide/en/elasticsearch/reference/current
The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.
==== Mapped field names
Without further configuration, Spring Data Elasticsearch will use the property name of an object as field name in Elasticsearch. This can be changed for individual field by using the `@Field` annotation on that property.
It is also possible to define a `FieldNamingStrategy` in the configuration of the client (<<elasticsearch.clients>>). If for example a `SnakeCaseFieldNamingStrategy` is configured, the property _sampleProperty_ of the object would be mapped to _sample_property_ in Elasticsearch. A `FieldNamingStrategy` applies to all entities; it can be overwritten by
setting a specific name with `@Field` on a property.
[[elasticsearch.mapping.meta-model.rules]]
=== Mapping Rules

View File

@ -33,6 +33,8 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@ -69,6 +71,7 @@ public class ElasticsearchConfigurationSupport {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
return mappingContext;
}
@ -160,4 +163,14 @@ public class ElasticsearchConfigurationSupport {
protected RefreshPolicy refreshPolicy() {
return null;
}
/**
* Configures a {@link FieldNamingStrategy} on the {@link SimpleElasticsearchMappingContext} instance created.
*
* @return the {@link FieldNamingStrategy} to use
* @since 4.2
*/
protected FieldNamingStrategy fieldNamingStrategy() {
return PropertyNameFieldNamingStrategy.INSTANCE;
}
}

View File

@ -16,9 +16,12 @@
package org.springframework.data.elasticsearch.core.mapping;
import org.springframework.data.mapping.context.AbstractMappingContext;
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.TypeInformation;
import org.springframework.lang.Nullable;
/**
* SimpleElasticsearchMappingContext
@ -31,6 +34,22 @@ import org.springframework.data.util.TypeInformation;
public class SimpleElasticsearchMappingContext
extends AbstractMappingContext<SimpleElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> {
private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
/**
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
* Defaults to a strategy using the plain property name.
*
* @param fieldNamingStrategy the {@link FieldNamingStrategy} to be used to determine the field name if no manual
* mapping is applied.
* @since 4.2
*/
public void setFieldNamingStrategy(@Nullable FieldNamingStrategy fieldNamingStrategy) {
this.fieldNamingStrategy = fieldNamingStrategy == null ? DEFAULT_NAMING_STRATEGY : fieldNamingStrategy;
}
@Override
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new SimpleElasticsearchPersistentEntity<>(typeInformation);
@ -39,6 +58,6 @@ public class SimpleElasticsearchMappingContext
@Override
protected ElasticsearchPersistentProperty createPersistentProperty(Property property,
SimpleElasticsearchPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder);
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy);
}
}

View File

@ -42,7 +42,9 @@ import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
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.lang.Nullable;
import org.springframework.util.StringUtils;
@ -72,13 +74,17 @@ public class SimpleElasticsearchPersistentProperty extends
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) {
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder,
@Nullable FieldNamingStrategy fieldNamingStrategy) {
super(property, owner, simpleTypeHolder);
this.annotatedFieldName = getAnnotatedFieldName();
this.fieldNamingStrategy = fieldNamingStrategy == null ? PropertyNameFieldNamingStrategy.INSTANCE
: fieldNamingStrategy;
this.isId = super.isIdProperty() || SUPPORTED_ID_PROPERTY_NAMES.contains(getFieldName());
// deprecated since 4.1
@ -233,7 +239,19 @@ public class SimpleElasticsearchPersistentProperty extends
@Override
public String getFieldName() {
return annotatedFieldName == null ? getProperty().getName() : annotatedFieldName;
if (annotatedFieldName == null) {
String fieldName = fieldNamingStrategy.getFieldName(this);
if (!StringUtils.hasText(fieldName)) {
throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s!",
this, fieldNamingStrategy.getClass()));
}
return fieldName;
}
return annotatedFieldName;
}
@Override

View File

@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.data.geo.Point;
/**
* @author P.J. Meisch (pj.meisch@sothawo.com)
* @author Peter-Josef Meisch
*/
class GeoPointUnitTests {
@ -36,7 +36,7 @@ class GeoPointUnitTests {
@DisplayName("should not be equal to a Point")
void shouldNotBeEqualToAPoint() {
//noinspection AssertBetweenInconvertibleTypes
// noinspection AssertBetweenInconvertibleTypes
assertThat(new GeoPoint(48, 8)).isNotEqualTo(new Point(8, 48));
}

View File

@ -0,0 +1,94 @@
/*
* 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.core.mapping;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import lombok.Builder;
import lombok.Data;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
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.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy;
import org.springframework.test.context.ContextConfiguration;
import reactor.test.StepVerifier;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { FieldNamingStrategyIntegrationReactiveTest.Config.class })
public class FieldNamingStrategyIntegrationReactiveTest {
@Autowired private ReactiveElasticsearchOperations operations;
@Configuration
static class Config extends ReactiveElasticsearchRestTemplateConfiguration {
@Override
protected FieldNamingStrategy fieldNamingStrategy() {
return new SnakeCaseFieldNamingStrategy();
}
}
@BeforeEach
void setUp() {
ReactiveIndexOperations indexOps = this.operations.indexOps(Entity.class);
indexOps.delete() //
.then(indexOps.create()) //
.then(indexOps.putMapping()) //
.block();
}
@Test // #1565
@DisplayName("should use configured FieldNameStrategy")
void shouldUseConfiguredFieldNameStrategy() {
Entity entity = new Entity.EntityBuilder().id("42").someText("the text to be searched").build();
operations.save(entity).block();
// use a native query here to prevent automatic property name matching
Query query = new NativeSearchQueryBuilder().withQuery(matchQuery("some_text", "searched")).build();
operations.search(query, Entity.class) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
}
@Data
@Builder
@Document(indexName = "field-naming-strategy-test")
static class Entity {
@Id private String id;
@Field(type = FieldType.Text) private String someText;
}
}

View File

@ -0,0 +1,27 @@
/*
* (c) Copyright 2021 sothawo
*/
package org.springframework.data.elasticsearch.core.mapping;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy;
import org.springframework.test.context.ContextConfiguration;
/**
* @author P.J. Meisch (pj.meisch@sothawo.com)
*/
@ContextConfiguration(classes = { FieldNamingStrategyIntegrationTemplateTest.Config.class })
public class FieldNamingStrategyIntegrationTemplateTest extends FieldNamingStrategyIntegrationTest {
@Configuration
static class Config extends ElasticsearchTemplateConfiguration {
@Override
protected FieldNamingStrategy fieldNamingStrategy() {
return new SnakeCaseFieldNamingStrategy();
}
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.core.mapping;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import lombok.Builder;
import lombok.Data;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
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.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { FieldNamingStrategyIntegrationTest.Config.class })
public class FieldNamingStrategyIntegrationTest {
@Autowired private ElasticsearchOperations operations;
@Configuration
static class Config extends ElasticsearchRestTemplateConfiguration {
@Override
@Bean
public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
RestHighLevelClient elasticsearchClient) {
return super.elasticsearchOperations(elasticsearchConverter, elasticsearchClient);
}
@Override
protected FieldNamingStrategy fieldNamingStrategy() {
return new SnakeCaseFieldNamingStrategy();
}
}
@BeforeEach
void setUp() {
IndexOperations indexOps = this.operations.indexOps(Entity.class);
indexOps.delete();
indexOps.create();
indexOps.putMapping();
}
@Test // #1565
@DisplayName("should use configured FieldNameStrategy")
void shouldUseConfiguredFieldNameStrategy() {
Entity entity = new Entity.EntityBuilder().id("42").someText("the text to be searched").build();
operations.save(entity);
// use a native query here to prevent automatic property name matching
Query query = new NativeSearchQueryBuilder().withQuery(matchQuery("some_text", "searched")).build();
SearchHits<Entity> searchHits = operations.search(query, Entity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
}
@Data
@Builder
@Document(indexName = "field-naming-strategy-test")
static class Entity {
@Id private String id;
@Field(type = FieldType.Text) private String someText;
}
}

View File

@ -152,7 +152,7 @@ public class SimpleElasticsearchPersistentEntityTests {
TypeInformation<?> type = entity.getTypeInformation();
Property property = Property.of(type, ReflectionUtils.findField(entity.getType(), field));
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT);
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT, null);
}

View File

@ -37,7 +37,13 @@ import org.springframework.data.elasticsearch.annotations.MultiField;
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.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
* Unit tests for {@link SimpleElasticsearchPersistentProperty}.
@ -200,6 +206,52 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
.withMessageContaining("pattern");
}
@Test // #1565
@DisplayName("should use default FieldNamingStrategy")
void shouldUseDefaultFieldNamingStrategy() {
ElasticsearchPersistentEntity<FieldNamingStrategyEntity> entity = new SimpleElasticsearchPersistentEntity<>(
ClassTypeInformation.from(FieldNamingStrategyEntity.class));
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);
assertThat(property.getFieldName()).isEqualTo("withoutCustomFieldName");
field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withCustomFieldName");
property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT,
null);
assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME");
}
@Test // #1565
@DisplayName("should use custom FieldNamingStrategy")
void shouldUseCustomFieldNamingStrategy() {
FieldNamingStrategy fieldNamingStrategy = new SnakeCaseFieldNamingStrategy();
ElasticsearchPersistentEntity<FieldNamingStrategyEntity> entity = new SimpleElasticsearchPersistentEntity<>(
ClassTypeInformation.from(FieldNamingStrategyEntity.class));
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);
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);
assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME");
}
static class InvalidScoreProperty {
@Nullable @Score String scoreProperty;
}
@ -245,4 +297,10 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
static class DateNanosFieldWithNoFormat {
@Field(type = FieldType.Date_Nanos) LocalDateTime datetime;
}
@Data
static class FieldNamingStrategyEntity {
private String withoutCustomFieldName;
@Field(name = "CUStomFIEldnAME") private String withCustomFieldName;
}
}

View File

@ -71,7 +71,8 @@ public class ElasticsearchRestTemplateConfiguration extends AbstractElasticsearc
@Override
public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
RestHighLevelClient elasticsearchClient) {
return new ElasticsearchRestTemplate(elasticsearchClient, elasticsearchConverter) {
ElasticsearchRestTemplate template = new ElasticsearchRestTemplate(elasticsearchClient, elasticsearchConverter) {
@Override
public <T> T execute(ClientCallback<T> callback) {
try {
@ -84,6 +85,9 @@ public class ElasticsearchRestTemplateConfiguration extends AbstractElasticsearc
}
}
};
template.setRefreshPolicy(refreshPolicy());
return template;
}
@Override

View File

@ -50,8 +50,10 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
@ -642,7 +644,11 @@ class SimpleElasticsearchRepositoryIntegrationTests {
sampleEntity.setMessage("some message");
// when
AbstractElasticsearchTemplate abstractElasticsearchTemplate = (AbstractElasticsearchTemplate) this.operations;
RefreshPolicy refreshPolicy = abstractElasticsearchTemplate.getRefreshPolicy();
abstractElasticsearchTemplate.setRefreshPolicy(RefreshPolicy.NONE);
repository.indexWithoutRefresh(sampleEntity);
abstractElasticsearchTemplate.setRefreshPolicy(refreshPolicy);
// then
Page<SampleEntity> entities = repository.search(termQuery("id", documentId), PageRequest.of(0, 50));