diff --git a/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc b/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc index 95babfbd2..060a05d46 100644 --- a/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc +++ b/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc @@ -1,8 +1,8 @@ [[elasticsearch.mapping]] = Elasticsearch Object Mapping -Spring Data Elasticsearch Object Mapping is the process that maps a Java object - the domain entity - into the JSON -representation that is stored in Elasticsearch and back. The class that is internally used for this mapping is the +Spring Data Elasticsearch Object Mapping is the process that maps a Java object - the domain entity - into the JSON representation that is stored in Elasticsearch and back. +The class that is internally used for this mapping is the `MappingElasticsearcvhConverter`. [[elasticsearch.mapping.meta-model]] @@ -52,21 +52,15 @@ The mapping metadata infrastructure is defined in a separate spring-data-commons [[elasticsearch.mapping.meta-model.annotations.read-write]] ==== Controlling which properties are written to and read from Elasticsearch -This section details the annotations that define if the value of a property is written to or -read from Elasticsearch. +This section details the annotations that define if the value of a property is written to or read from Elasticsearch. -`@Transient`: A property annotated with this annotation will not be written to the mapping, it's value will not be -sent to Elasticsearch and when documents are returned from Elasticsearch, this property will not be set in the -resulting entity. +`@Transient`: A property annotated with this annotation will not be written to the mapping, it's value will not be sent to Elasticsearch and when documents are returned from Elasticsearch, this property will not be set in the resulting entity. -`@ReadOnlyProperty`: A property with this annotaiton will not have its value written to Elasticsearch, but when -returning data, the proeprty will be filled with the value returned in the document from Elasticsearch. One use case -for this are runtime fields defined in the index mapping. - -`@WriteOnlyProperty`: A property with this annotaiton will have its value stored in Elasticsearch but will not be set -with any value when reading document. This can be used for example for synthesized fields which should go into the -Elasticsearch index but are not used elsewhere. +`@ReadOnlyProperty`: A property with this annotaiton will not have its value written to Elasticsearch, but when returning data, the proeprty will be filled with the value returned in the document from Elasticsearch. +One use case for this are runtime fields defined in the index mapping. +`@WriteOnlyProperty`: A property with this annotaiton will have its value stored in Elasticsearch but will not be set with any value when reading document. +This can be used for example for synthesized fields which should go into the Elasticsearch index but are not used elsewhere. [[elasticsearch.mapping.meta-model.annotations.date-formats]] ==== Date format mapping @@ -110,8 +104,7 @@ The following table shows the different attributes and the mapping created from NOTE: If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_. This is due to a https://www.elastic.co/guide/en/elasticsearch/reference/current/migrate-to-java-time.html#java-time-migration-incompatible-date-formats[change in Elasticsearch 7]. -Check the code of the `org.springframework.data.elasticsearch.annotations.DateFormat` enum for a complete list of -predefined values and their patterns. +Check the code of the `org.springframework.data.elasticsearch.annotations.DateFormat` enum for a complete list of predefined values and their patterns. [[elasticsearch.mapping.meta-model.annotations.range]] ==== Range types @@ -172,12 +165,13 @@ A `FieldNamingStrategy` applies to all entities; it can be overwritten by settin [[elasticsearch.mapping.meta-model.annotations.non-field-backed-properties]] ==== Non-field-backed properties -Normally the properties used in an entity are fields of the entity class. There might be cases, when a property value -is calculated in the entity and should be stored in Elasticsearch. In this case, the getter method (`getProperty()`) can be -annotated -with the `@Field` annotation, in addition to that the method must be annotated with `@AccessType(AccessType.Type -.PROPERTY)`. The third annotation that is needed in such a case is `@WriteOnlyProperty`, as such a value is only -written to Elasticsearch. A full example: +Normally the properties used in an entity are fields of the entity class. +There might be cases, when a property value is calculated in the entity and should be stored in Elasticsearch. +In this case, the getter method (`getProperty()`) can be annotated with the `@Field` annotation, in addition to that the method must be annotated with `@AccessType(AccessType.Type +.PROPERTY)`. +The third annotation that is needed in such a case is `@WriteOnlyProperty`, as such a value is only written to Elasticsearch. +A full example: + ==== [source,java] ---- @@ -190,6 +184,19 @@ public String getProperty() { ---- ==== +[[elasticsearch.mapping.meta-model.annotations.misc]] +==== Other property annotations + +===== @IndexedIndexName + +This annotation can be set on a String property of an entity. +This property will not be written to the mapping, it will not be stored in Elasticsearch and its value will not be read from an Elasticsearch document. +After an entity is persisted, for example with a call to `ElasticsearchOperations.save(T entity)`, the entity +returned from that call will contain the name of the index that an entity was saved to in that property. +This is useful when the index name is dynamically set by a bean, or when writing to a write alias. + +Putting some value into such a property does not set the index into which an entity is stored! + [[elasticsearch.mapping.meta-model.rules]] === Mapping Rules @@ -412,12 +419,15 @@ Looking at the `Configuration` from the < static class AddressToMap implements Converter> { - + @Override public Map convert(Address source) { diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java new file mode 100644 index 000000000..1a15f506c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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.annotation.ReadOnlyProperty; +import org.springframework.data.annotation.Transient; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a String property of an entity to be filled with the name of the index where the entity was + * stored after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created + * or when a document was indexed into a write alias. + * + * This can not be used to specify the index where an entity should be written to. + * + * @author Peter-Josef Meisch + * @since 5.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +@Documented +@Field(type = FieldType.Auto) // prevents the property being written to the index mapping +public @interface IndexedIndexName { +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java index 11c4c6a9c..951b54687 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java @@ -217,8 +217,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { Object queryObject = query.getObject(); if (queryObject != null) { - query.setObject(updateIndexedObject(queryObject, IndexedObjectInformation.of(indexResponse.id(), - indexResponse.seqNo(), indexResponse.primaryTerm(), indexResponse.version()))); + query.setObject(updateIndexedObject(queryObject, new IndexedObjectInformation(indexResponse.id(), + indexResponse.index(), indexResponse.seqNo(), indexResponse.primaryTerm(), indexResponse.version()))); } return indexResponse.id(); @@ -629,7 +629,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { } return bulkResponse.items().stream() - .map(item -> IndexedObjectInformation.of(item.id(), item.seqNo(), item.primaryTerm(), item.version())) + .map(item -> new IndexedObjectInformation(item.id(), item.index(), item.seqNo(), item.primaryTerm(), + item.version())) .collect(Collectors.toList()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java index 6a4bec6cb..501fc7504 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java @@ -111,7 +111,9 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch return Mono.just(entity) // .zipWith(// Mono.from(execute((ClientCallback>) client -> client.index(indexRequest))) // - .map(indexResponse -> new IndexResponseMetaData(indexResponse.id(), // + .map(indexResponse -> new IndexResponseMetaData( + indexResponse.id(), // + indexResponse.index(), // indexResponse.seqNo(), // indexResponse.primaryTerm(), // indexResponse.version() // @@ -139,8 +141,12 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch .flatMap(indexAndResponse -> { T savedEntity = entities.entityAt(indexAndResponse.getT1()); BulkResponseItem response = indexAndResponse.getT2(); - updateIndexedObject(savedEntity, IndexedObjectInformation.of(response.id(), response.seqNo(), - response.primaryTerm(), response.version())); + updateIndexedObject(savedEntity, new IndexedObjectInformation( // + response.id(), // + response.index(), // + response.seqNo(), // + response.primaryTerm(), // + response.version())); return maybeCallbackAfterSave(savedEntity, index); }); }); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ElasticsearchRestTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ElasticsearchRestTemplate.java index cc92b5716..4eff9b7bd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ElasticsearchRestTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ElasticsearchRestTemplate.java @@ -194,8 +194,12 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate { Object queryObject = query.getObject(); if (queryObject != null) { - query.setObject(updateIndexedObject(queryObject, IndexedObjectInformation.of(indexResponse.getId(), - indexResponse.getSeqNo(), indexResponse.getPrimaryTerm(), indexResponse.getVersion()))); + query.setObject(updateIndexedObject(queryObject, new IndexedObjectInformation( // + indexResponse.getId(), // + indexResponse.getIndex(), // + indexResponse.getSeqNo(), // + indexResponse.getPrimaryTerm(), // + indexResponse.getVersion()))); } return indexResponse.getId(); @@ -369,10 +373,15 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate { return Stream.of(bulkResponse.getItems()).map(bulkItemResponse -> { DocWriteResponse response = bulkItemResponse.getResponse(); if (response != null) { - return IndexedObjectInformation.of(response.getId(), response.getSeqNo(), response.getPrimaryTerm(), + return new IndexedObjectInformation( // + response.getId(), // + response.getIndex(), // + response.getSeqNo(), // + response.getPrimaryTerm(), // response.getVersion()); } else { - return IndexedObjectInformation.of(bulkItemResponse.getId(), null, null, null); + return new IndexedObjectInformation(bulkItemResponse.getId(), bulkItemResponse.getIndex(), null, null, + null); } }).collect(Collectors.toList()); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ReactiveElasticsearchTemplate.java index 9b95a37e2..782d3ae49 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/ReactiveElasticsearchTemplate.java @@ -155,8 +155,8 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch BulkItemResponse bulkItemResponse = indexAndResponse.getT2(); DocWriteResponse response = bulkItemResponse.getResponse(); - updateIndexedObject(savedEntity, IndexedObjectInformation.of(response.getId(), response.getSeqNo(), - response.getPrimaryTerm(), response.getVersion())); + updateIndexedObject(savedEntity, new IndexedObjectInformation(response.getId(), response.getIndex(), + response.getSeqNo(), response.getPrimaryTerm(), response.getVersion())); return maybeCallbackAfterSave(savedEntity, index); }); @@ -255,10 +255,11 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch return Mono.just(entity).zipWith(doIndex(request) // .map(indexResponse -> new IndexResponseMetaData( // indexResponse.getId(), // + indexResponse.getIndex(), // indexResponse.getSeqNo(), // indexResponse.getPrimaryTerm(), // indexResponse.getVersion() // - ))); // + ))); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java index 22af9b92d..9b7a99990 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java @@ -408,23 +408,29 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty(); // Only deal with text because ES generated Ids are strings! - if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isReadable() + if (indexedObjectInformation.id() != null && idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) { - propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId()); + propertyAccessor.setProperty(idProperty, indexedObjectInformation.id()); } - if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null + if (indexedObjectInformation.seqNo() != null && indexedObjectInformation.primaryTerm() != null && persistentEntity.hasSeqNoPrimaryTermProperty()) { ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty(); // noinspection ConstantConditions propertyAccessor.setProperty(seqNoPrimaryTermProperty, - new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm())); + new SeqNoPrimaryTerm(indexedObjectInformation.seqNo(), + indexedObjectInformation.primaryTerm())); } - if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) { + if (indexedObjectInformation.version() != null && persistentEntity.hasVersionProperty()) { ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty(); // noinspection ConstantConditions - propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion()); + propertyAccessor.setProperty(versionProperty, indexedObjectInformation.version()); + } + + var indexedIndexNameProperty = persistentEntity.getIndexedIndexNameProperty(); + if (indexedIndexNameProperty != null) { + propertyAccessor.setProperty(indexedIndexNameProperty, indexedObjectInformation.index()); } // noinspection unchecked @@ -791,8 +797,9 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper T entity = reader.read(type, documentAfterLoad); - IndexedObjectInformation indexedObjectInformation = IndexedObjectInformation.of( // + IndexedObjectInformation indexedObjectInformation = new IndexedObjectInformation( // documentAfterLoad.hasId() ? documentAfterLoad.getId() : null, // + documentAfterLoad.getIndex(), // documentAfterLoad.hasSeqNo() ? documentAfterLoad.getSeqNo() : null, // documentAfterLoad.hasPrimaryTerm() ? documentAfterLoad.getPrimaryTerm() : null, // documentAfterLoad.hasVersion() ? documentAfterLoad.getVersion() : null); // diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java index fc7e7f8b9..ff85e88e0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java @@ -261,23 +261,28 @@ abstract public class AbstractReactiveElasticsearchTemplate ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty(); // Only deal with text because ES generated Ids are strings! - if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isReadable() + if (indexedObjectInformation.id() != null && idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) { - propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId()); + propertyAccessor.setProperty(idProperty, indexedObjectInformation.id()); } - if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null + if (indexedObjectInformation.seqNo() != null && indexedObjectInformation.primaryTerm() != null && persistentEntity.hasSeqNoPrimaryTermProperty()) { ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty(); // noinspection ConstantConditions propertyAccessor.setProperty(seqNoPrimaryTermProperty, - new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm())); + new SeqNoPrimaryTerm(indexedObjectInformation.seqNo(), indexedObjectInformation.primaryTerm())); } - if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) { + if (indexedObjectInformation.version() != null && persistentEntity.hasVersionProperty()) { ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty(); // noinspection ConstantConditions - propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion()); + propertyAccessor.setProperty(versionProperty, indexedObjectInformation.version()); + } + + var indexedIndexNameProperty = persistentEntity.getIndexedIndexNameProperty(); + if (indexedIndexNameProperty != null) { + propertyAccessor.setProperty(indexedIndexNameProperty, indexedObjectInformation.index()); } // noinspection unchecked @@ -286,7 +291,7 @@ abstract public class AbstractReactiveElasticsearchTemplate } else { EntityOperations.AdaptableEntity adaptableEntity = entityOperations.forEntity(entity, converter.getConversionService(), routingResolver); - adaptableEntity.populateIdIfNecessary(indexedObjectInformation.getId()); + adaptableEntity.populateIdIfNecessary(indexedObjectInformation.id()); } return entity; } @@ -317,8 +322,9 @@ abstract public class AbstractReactiveElasticsearchTemplate .map(it -> { T savedEntity = it.getT1(); IndexResponseMetaData indexResponseMetaData = it.getT2(); - return updateIndexedObject(savedEntity, IndexedObjectInformation.of( // + return updateIndexedObject(savedEntity, new IndexedObjectInformation( // indexResponseMetaData.id(), // + indexResponseMetaData.index(), // indexResponseMetaData.seqNo(), // indexResponseMetaData.primaryTerm(), // indexResponseMetaData.version())); @@ -571,8 +577,9 @@ abstract public class AbstractReactiveElasticsearchTemplate T entity = reader.read(type, documentAfterLoad); - IndexedObjectInformation indexedObjectInformation = IndexedObjectInformation.of( // + IndexedObjectInformation indexedObjectInformation = new IndexedObjectInformation( // documentAfterLoad.hasId() ? documentAfterLoad.getId() : null, // + documentAfterLoad.getIndex(), // documentAfterLoad.hasSeqNo() ? documentAfterLoad.getSeqNo() : null, // documentAfterLoad.hasPrimaryTerm() ? documentAfterLoad.getPrimaryTerm() : null, // documentAfterLoad.hasVersion() ? documentAfterLoad.getVersion() : null); // @@ -685,7 +692,7 @@ abstract public class AbstractReactiveElasticsearchTemplate /** * Value class to capture client independent information from a response to an index request. */ - public record IndexResponseMetaData(String id, long seqNo, long primaryTerm, long version) { + public record IndexResponseMetaData(String id, String index, long seqNo, long primaryTerm, long version) { } // endregion diff --git a/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java b/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java index 3ce788bfd..1c7d95f4d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java @@ -24,42 +24,12 @@ import org.springframework.lang.Nullable; * @author Roman Puchkovskiy * @since 4.1 */ -public class IndexedObjectInformation { - @Nullable private final String id; - @Nullable private final Long seqNo; - @Nullable private final Long primaryTerm; - @Nullable private final Long version; - - private IndexedObjectInformation(@Nullable String id, @Nullable Long seqNo, @Nullable Long primaryTerm, - @Nullable Long version) { - this.id = id; - this.seqNo = seqNo; - this.primaryTerm = primaryTerm; - this.version = version; - } - - public static IndexedObjectInformation of(@Nullable String id, @Nullable Long seqNo, @Nullable Long primaryTerm, - @Nullable Long version) { - return new IndexedObjectInformation(id, seqNo, primaryTerm, version); - } - - @Nullable - public String getId() { - return id; - } - - @Nullable - public Long getSeqNo() { - return seqNo; - } - - @Nullable - public Long getPrimaryTerm() { - return primaryTerm; - } - - @Nullable - public Long getVersion() { - return version; - } +public record IndexedObjectInformation( // + @Nullable String id, // + /** @since 5.1 */ // + @Nullable String index, // + @Nullable Long seqNo, // + @Nullable Long primaryTerm, // + @Nullable Long version // +) { } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index e1ebd09b3..0562dd180 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -409,15 +409,15 @@ public class MappingElasticsearchConverter PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), conversionService); - for (ElasticsearchPersistentProperty prop : entity) { + for (ElasticsearchPersistentProperty property : entity) { - if (entity.isCreatorArgument(prop) || !prop.isReadable()) { + if (entity.isCreatorArgument(property) || !property.isReadable() || property.isIndexedIndexNameProperty()) { continue; } - Object value = valueProvider.getPropertyValue(prop); + Object value = valueProvider.getPropertyValue(property); if (value != null) { - accessor.setProperty(prop, value); + accessor.setProperty(property, value); } } @@ -939,7 +939,7 @@ public class MappingElasticsearchConverter for (ElasticsearchPersistentProperty property : entity) { - if (!property.isWritable()) { + if (!property.isWritable() || property.isIndexedIndexNameProperty()) { continue; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java b/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java index 26fb3d806..8ea130ac6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java @@ -107,7 +107,7 @@ public interface Document extends StringObjectMap { } /** - * @return the index if this document was retrieved from an index + * @return the index if this document was retrieved from an index or was just stored. * @since 4.1 */ @Nullable diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java index e7f174fda..86ec740eb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java @@ -133,6 +133,14 @@ public interface ElasticsearchPersistentEntity extends PersistentEntity extends BasicPersistentEntit private final Lazy settingsParameter; private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty; private @Nullable ElasticsearchPersistentProperty joinFieldProperty; + private @Nullable ElasticsearchPersistentProperty indexedIndexNameProperty; private @Nullable Document.VersionType versionType; private boolean createIndexAndMapping; private final Dynamic dynamic; @@ -218,6 +220,20 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit } } + if (property.isIndexedIndexNameProperty()) { + + if (!property.getActualType().isAssignableFrom(String.class)) { + throw new MappingException(String.format("@IndexedIndexName annotation must be put on String property")); + } + + if (indexedIndexNameProperty != null) { + throw new MappingException( + String.format("@IndexedIndexName annotation can only be put on one property in an entity")); + } + + this.indexedIndexNameProperty = property; + } + Class actualType = property.getActualTypeOrNull(); if (actualType == JoinField.class) { ElasticsearchPersistentProperty joinProperty = this.joinFieldProperty; @@ -280,6 +296,12 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit return joinFieldProperty; } + @Nullable + @Override + public ElasticsearchPersistentProperty getIndexedIndexNameProperty() { + return indexedIndexNameProperty; + } + // region SpEL handling /** * resolves all the names in the IndexCoordinates object. If a name cannot be resolved, the original name is returned. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index cde571c8b..9a538b0ed 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -31,6 +31,7 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.GeoPointField; import org.springframework.data.elasticsearch.annotations.GeoShapeField; +import org.springframework.data.elasticsearch.annotations.IndexedIndexName; import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.annotations.ValueConverter; import org.springframework.data.elasticsearch.annotations.WriteOnlyProperty; @@ -358,4 +359,8 @@ public class SimpleElasticsearchPersistentProperty extends return getActualType() == Completion.class; } + @Override + public boolean isIndexedIndexNameProperty() { + return isAnnotationPresent(IndexedIndexName.class); + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java index a16f87369..57788d458 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java @@ -58,16 +58,9 @@ import org.springframework.data.annotation.Version; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.*; import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; -import org.springframework.data.elasticsearch.annotations.InnerField; -import org.springframework.data.elasticsearch.annotations.JoinTypeRelation; -import org.springframework.data.elasticsearch.annotations.JoinTypeRelations; -import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.annotations.ScriptedField; -import org.springframework.data.elasticsearch.annotations.Setting; -import org.springframework.data.elasticsearch.annotations.WriteOnlyProperty; import org.springframework.data.elasticsearch.client.erhlc.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.document.Explanation; import org.springframework.data.elasticsearch.core.geo.GeoPoint; @@ -78,6 +71,7 @@ import org.springframework.data.elasticsearch.core.index.Settings; import org.springframework.data.elasticsearch.core.join.JoinField; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.*; +import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.highlight.Highlight; import org.springframework.data.elasticsearch.core.query.highlight.HighlightField; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; @@ -137,6 +131,7 @@ public abstract class ElasticsearchIntegrationTests { indexNameProvider.increment(); indexOperations = operations.indexOps(SampleEntity.class); indexOperations.createWithMapping(); + operations.indexOps(IndexedIndexNameEntity.class).createWithMapping(); } @Test @@ -3573,6 +3568,18 @@ public abstract class ElasticsearchIntegrationTests { operations.index(query, IndexCoordinates.of(indexNameProvider.indexName())); } + @Test // #2112 + @DisplayName("should set IndexedIndexName property") + void shouldSetIndexedIndexNameProperty() { + + var entity = new IndexedIndexNameEntity(); + entity.setId("42"); + entity.setSomeText("someText"); + var saved = operations.save(entity); + + assertThat(saved.getIndexedIndexName()).isEqualTo(indexNameProvider.indexName() + "-indexedindexname"); + } + @Test // #1945 @DisplayName("should error on sort with unmapped field and default settings") void shouldErrorOnSortWithUnmappedFieldAndDefaultSettings() { @@ -4663,5 +4670,42 @@ public abstract class ElasticsearchIntegrationTests { return result; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}-indexedindexname") + private static class IndexedIndexNameEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @IndexedIndexName private String indexedIndexName; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getSomeText() { + return someText; + } + + public void setSomeText(@Nullable String someText) { + this.someText = someText; + } + + @Nullable + public String getIndexedIndexName() { + return indexedIndexName; + } + + public void setIndexedIndexName(@Nullable String indexedIndexName) { + this.indexedIndexName = indexedIndexName; + } + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java index 556063fa6..ab18dd3c2 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import static org.elasticsearch.index.query.QueryBuilders.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import org.springframework.data.elasticsearch.annotations.IndexedIndexName; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -105,6 +106,7 @@ public abstract class ReactiveElasticsearchIntegrationTests { indexNameProvider.increment(); operations.indexOps(SampleEntity.class).createWithMapping().block(); + operations.indexOps(IndexedIndexNameEntity.class).createWithMapping().block(); } @Test @@ -156,6 +158,19 @@ public abstract class ReactiveElasticsearchIntegrationTests { .verifyComplete(); } + @Test // #2112 + @DisplayName("should set IndexedIndexName property") + void shouldSetIndexedIndexNameProperty() { + + var entity = new IndexedIndexNameEntity(); + entity.setId("42"); + entity.setSomeText("someText"); + var saved = operations.save(entity).block(); + + assertThat(saved.getIndexedIndexName()).isEqualTo(indexNameProvider.indexName() + "-indexedindexname"); + } + + private Mono documentWithIdExistsInIndex(String id, String index) { return operations.exists(id, IndexCoordinates.of(index)); } @@ -1528,5 +1543,41 @@ public abstract class ReactiveElasticsearchIntegrationTests { this.part2 = part2; } } - // endregion + @Document(indexName = "#{@indexNameProvider.indexName()}-indexedindexname") + private static class IndexedIndexNameEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @IndexedIndexName + private String indexedIndexName; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getSomeText() { + return someText; + } + + public void setSomeText(@Nullable String someText) { + this.someText = someText; + } + + @Nullable + public String getIndexedIndexName() { + return indexedIndexName; + } + + public void setIndexedIndexName(@Nullable String indexedIndexName) { + this.indexedIndexName = indexedIndexName; + } + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index 85d7c352d..11d256f91 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -1043,6 +1043,28 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { assertEquals(expected, mapping, true); } + @Test // #2112 + @DisplayName("should not write mapping for property with IndexedIndexName anotation") + void shouldNotWriteMappingForPropertyWithIndexedIndexNameAnotation() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "someText": { + "type": "text" + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(IndexedIndexNameEntity.class); + + assertEquals(expected, mapping, true); + } // region entities @Document(indexName = "ignore-above-index") @@ -2193,5 +2215,13 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { @Nullable @Field(name = "excluded-text", type = Text, excludeFromSource = true) private String excludedText; } + + private static class IndexedIndexNameEntity { + @Nullable + @Field(type = Text) private String someText; + @Nullable + @IndexedIndexName + private String storedIndexName; + } // endregion }