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:
committed by
GitHub
parent
29821baed6
commit
8cc6da4b98
@@ -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]
|
||||
|
||||
+19
-37
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -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 {}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user