Skip to content

Use FieldNamingStrategy for property name matching. #1648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/asciidoc/reference/elasticsearch-object-mapping.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
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;
Expand Down Expand Up @@ -69,6 +71,7 @@ public SimpleElasticsearchMappingContext elasticsearchMappingContext(
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());

return mappingContext;
}
Expand Down Expand Up @@ -160,4 +163,14 @@ protected Set<Class<?>> scanForEntities(String basePackage) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +34,22 @@
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);
Expand All @@ -39,6 +58,6 @@ protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(Type
@Override
protected ElasticsearchPersistentProperty createPersistentProperty(Property property,
SimpleElasticsearchPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder);
return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -233,7 +239,19 @@ private String getAnnotatedFieldName() {

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.springframework.data.geo.Point;

/**
* @author P.J. Meisch ([email protected])
* @author Peter-Josef Meisch
*/
class GeoPointUnitTests {

Expand Down Expand Up @@ -36,7 +36,7 @@ void shouldConvertToAPoint() {
@DisplayName("should not be equal to a Point")
void shouldNotBeEqualToAPoint() {

//noinspection AssertBetweenInconvertibleTypes
// noinspection AssertBetweenInconvertibleTypes
assertThat(new GeoPoint(48, 8)).isNotEqualTo(new Point(8, 48));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
*/
@ContextConfiguration(classes = { FieldNamingStrategyIntegrationTemplateTest.Config.class })
public class FieldNamingStrategyIntegrationTemplateTest extends FieldNamingStrategyIntegrationTest {

@Configuration
static class Config extends ElasticsearchTemplateConfiguration {

@Override
protected FieldNamingStrategy fieldNamingStrategy() {
return new SnakeCaseFieldNamingStrategy();
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasti

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);

}

Expand Down
Loading