1
0
mirror of synced 2026-08-05 00:17:03 +00:00

Implement property resolution for index names.

* update index name resolution implementation
* documentation and refactoring

Closes: #3310
Original Pull Request: #3322

---------

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
This commit is contained in:
Peter-Josef Meisch
2026-07-29 07:39:54 +02:00
committed by GitHub
parent 29821baed6
commit 8cc6da4b98
6 changed files with 235 additions and 38 deletions
@@ -7,6 +7,7 @@
* Upgrade to Elasticsearch 9.4.4
* Allow to provide non-standard (custom) index options for `@Field` / `@InnerField`
* Support Elasticsearch Serverless
* Support propert resolution in index name expressions (https://github.com/spring-projects/spring-data-elasticsearch/issues/3310[GH #3310])
[[new-features.6-1-0]]
== New in Spring Data Elasticsearch 6.1
@@ -22,7 +22,7 @@ The following annotations are available:
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database.
The most important attributes are (check the API documentation for the complete list of attributes):
** `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()}"`
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` or a property expression like `{index.name}`
** `createIndex`: flag whether to create an index on repository bootstrapping.
Default value is _true_.
See xref:elasticsearch/repositories/elasticsearch-repositories.adoc#elasticsearch.repositories.autocreation[Automatic creation of indices with the corresponding mapping]
@@ -38,6 +38,9 @@ import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.utils.spel.ExpressionUtils;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
@@ -52,6 +55,7 @@ import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -75,7 +79,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private @Nullable final Document document;
private @Nullable String indexName;
private final String unresolvedIndexName;
private final Lazy<SettingsParameter> settingsParameter;
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
@@ -90,8 +94,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final ContextConfiguration contextConfiguration;
private final Set<Alias> aliases = new HashSet<>();
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
private final Lazy<ValueEvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
private final boolean storeIdInSource;
private final boolean storeVersionInSource;
@@ -111,10 +114,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz));
if (document != null) {
Assert.hasText(document.indexName(),
" Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")");
this.indexName = document.indexName();
this.unresolvedIndexName = document.indexName();
this.versionType = document.versionType();
this.createIndexAndMapping = document.createIndex();
this.alwaysWriteMapping = document.alwaysWriteMapping();
@@ -123,6 +126,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.storeVersionInSource = document.storeVersionInSource();
buildAliases();
} else {
this.unresolvedIndexName = getTypeInformation().getType().getSimpleName();
this.dynamic = Dynamic.INHERIT;
this.storeIdInSource = true;
this.storeVersionInSource = true;
@@ -139,13 +143,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
}
private String getIndexName() {
return indexName != null ? indexName : getTypeInformation().getType().getSimpleName();
}
@Override
public IndexCoordinates getIndexCoordinates() {
return resolve(IndexCoordinates.of(getIndexName()));
return resolve(IndexCoordinates.of(unresolvedIndexName));
}
@Override
@@ -376,42 +376,24 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Assert.notNull(name, "name must not be null");
Expression expression = getExpressionForIndexName(name);
ValueExpression expression = ExpressionUtils.detectExpression(name);
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
: null;
return resolvedName != null ? resolvedName : name;
Object resolvedName = expression != null ? expression.evaluate(indexNameEvaluationContext.get()) : null;
return resolvedName != null ? ObjectUtils.nullSafeToString(resolvedName) : name;
}
/**
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
* returns {@literal null}.
*
* @param name the name to get the expression for
* @return Expression may be null
*/
@Nullable
private Expression getExpressionForIndexName(String name) {
return indexNameExpressions.computeIfAbsent(name, s -> {
Expression expr = PARSER.parseExpression(s, ParserContext.TEMPLATE_EXPRESSION);
return expr instanceof LiteralExpression ? null : expr;
});
}
/**
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
* {@link #getIndexName()}.
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the unresolvedIndexName.
*
* @return EvaluationContext
*/
private EvaluationContext getIndexNameEvaluationContext() {
private ValueEvaluationContext getIndexNameEvaluationContext() {
Expression expression = getExpressionForIndexName(getIndexName());
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
: ExpressionDependencies.none();
ValueExpression expression = ExpressionUtils.detectExpression(unresolvedIndexName);
var expressionDependencies = expression != null ? expression.getExpressionDependencies() : null;
// noinspection ConstantConditions
return getEvaluationContext(null, expressionDependencies);
return expressionDependencies != null ? getValueEvaluationContext(null, expressionDependencies)
: getValueEvaluationContext(null);
}
@Override
@@ -0,0 +1,81 @@
/*
* Copyright 2021-present 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.utils.spel;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
/**
* Internal utility class for dealing with {@link ValueExpression} and potential ones. Shamelessly copied from Spring
* Data MongoDB. Adapted afterwards to our needs.
*
* @author Christoph Strobl
* @since 6.2
*/
public final class ExpressionUtils {
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
private static final ConcurrentHashMap<String, ValueExpression> expressionCache = new ConcurrentHashMap<>();
/**
* Returns a SpEL {@link ValueExpression} if the given {@link String} is not empty. ValueExpressions are stored in a
* cash.
*
* @param potentialExpression can be {@literal null}
* @return {@link ValueExpression} or null when input is empty
*/
@Contract("null -> null")
public static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
return expressionCache.computeIfAbsent(potentialExpression,
key -> PARSER.parse(potentialExpression));
}
/**
* evbaluates value against the context provided by valueEvaluationContextSupplier. If value cannot be transformed
* into a {@link ValueExpression}, the defaultValue is returned.
*
* @param value the value to evaluate
* @param valueEvaluationContextSupplier supplies the context to use for evaluation
* @param defaultValue
* @return
*/
public static @Nullable Object evaluate(String value, Supplier<ValueEvaluationContext> valueEvaluationContextSupplier,
@Nullable Object defaultValue) {
ValueExpression expression = detectExpression(value);
if (expression == null) {
return defaultValue;
}
var evaluated = expression.evaluate(valueEvaluationContextSupplier.get());
return evaluated == null ? defaultValue : evaluated;
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2024-present 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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
public class SimpleElasticsearchPersistentEntityELCIntegrationTests extends SimpleElasticsearchPersistentEntityIntegrationTests {
@Configuration
static class Config extends ElasticsearchTemplateConfiguration {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("persistent-entity");
}
}
}
@@ -0,0 +1,100 @@
/*
* Copyright 2021-present 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;
/*
* Copyright 2024-present 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.
*/
import static org.assertj.core.api.Assertions.*;
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.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.TestPropertySource;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@TestPropertySource(properties = { "entity.indexName = index-property" })
public abstract class SimpleElasticsearchPersistentEntityIntegrationTests {
@Autowired private ElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@BeforeEach
void setUp() {
indexNameProvider.increment();
}
@Test // #3310
@DisplayName("should evaluate static index name")
void shouldEvaluateStaticIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithStaticName.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("static-name");
}
@Test // #3310
@DisplayName("should evaluate SpEL index name")
void shouldEvaluateSpELIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithSpel.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo(indexNameProvider.indexName());
}
@Test // #3310
@DisplayName("should evaluate property index name")
void shouldEvaluatePropertyIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithProperty.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("index-property");
}
private ElasticsearchPersistentEntity<?> getRequiredPersistentEntity(Class<?> clazz) {
return operations.getElasticsearchConverter().getMappingContext().getRequiredPersistentEntity(clazz);
}
@Document(indexName = "static-name")
static class EntityWithStaticName {}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class EntityWithSpel {}
@Document(indexName = "${entity.indexName}")
static class EntityWithProperty {}
}