Skip to content

Commit d9bf76f

Browse files
authored
Fix reading response runtime field by mapping.
Original Pull Request #2432 Closes #2431
1 parent 4d11a56 commit d9bf76f

10 files changed

+119
-44
lines changed

src/main/asciidoc/reference/elasticsearch-object-mapping.adoc

+20-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ See <<elasticsearch.repositories.autocreation>>
2929

3030

3131
* `@Id`: Applied at the field level to mark the field used for identity purpose.
32-
* `@Transient`: By default all fields are mapped to the document when it is stored or retrieved, this annotation excludes the field.
32+
* `@Transient`, `@ReadOnlyProperty`, `@WriteOnlyProperty`: see the following section <<elasticsearch.mapping.meta-model.annotations.read-write>> for detailed information.
3333
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database.
3434
Constructor arguments are mapped by name to the key values in the retrieved Document.
3535
* `@Field`: Applied at the field level and defines properties of the field, most of the attributes map to the respective https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html[Elasticsearch Mapping] definitions (the following list is not complete, check the annotation Javadoc for a complete reference):
@@ -49,6 +49,25 @@ In difference to a registered Spring `Converter` this only converts the annotate
4949

5050
The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.
5151

52+
[[elasticsearch.mapping.meta-model.annotations.read-write]]
53+
==== Controlling which properties are written to and read from Elasticsearch
54+
55+
This section details the annotations that define if the value of a property is written to or
56+
read from Elasticsearch.
57+
58+
`@Transient`: A property annotated with this annotation will not be written to the mapping, it's value will not be
59+
sent to Elasticsearch and when documents are returned from Elasticsearch, this property will not be set in the
60+
resulting entity.
61+
62+
`@ReadOnlyProperty`: A property with this annotaiton will not have its value written to Elasticsearch, but when
63+
returning data, the proeprty will be filled with the value returned in the document from Elasticsearch. One use case
64+
for this are runtime fields defined in the index mapping.
65+
66+
`@WriteOnlyProperty`: A property with this annotaiton will have its value stored in Elasticsearch but will not be set
67+
with any value when reading document. This can be used for example for synthesized fields which should go into the
68+
Elasticsearch index but are not used elsewhere.
69+
70+
5271
[[elasticsearch.mapping.meta-model.annotations.date-formats]]
5372
==== Date format mapping
5473

src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ protected <T> T updateIndexedObject(T entity, IndexedObjectInformation indexedOb
408408
ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
409409

410410
// Only deal with text because ES generated Ids are strings!
411-
if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isWritable()
411+
if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isReadable()
412412
&& idProperty.getType().isAssignableFrom(String.class)) {
413413
propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId());
414414
}

src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ protected <T> T updateIndexedObject(T entity, IndexedObjectInformation indexedOb
261261
ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
262262

263263
// Only deal with text because ES generated Ids are strings!
264-
if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isWritable()
264+
if (indexedObjectInformation.getId() != null && idProperty != null && idProperty.isReadable()
265265
&& idProperty.getType().isAssignableFrom(String.class)) {
266266
propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId());
267267
}

src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ private <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Ob
349349
PersistentPropertyAccessor<R> propertyAccessor = new ConvertingPropertyAccessor<>(
350350
targetEntity.getPropertyAccessor(result), conversionService);
351351
// Only deal with String because ES generated Ids are strings !
352-
if (idProperty != null && idProperty.isWritable() && idProperty.getType().isAssignableFrom(String.class)) {
352+
if (idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) {
353353
propertyAccessor.setProperty(idProperty, document.getId());
354354
}
355355
}
@@ -411,7 +411,7 @@ protected <R> R readProperties(ElasticsearchPersistentEntity<?> entity, R instan
411411

412412
for (ElasticsearchPersistentProperty prop : entity) {
413413

414-
if (entity.isCreatorArgument(prop) || !prop.isReadable() || !prop.isWritable()) {
414+
if (entity.isCreatorArgument(prop) || !prop.isReadable()) {
415415
continue;
416416
}
417417

src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
6161
PropertyValueConverter getPropertyValueConverter();
6262

6363
/**
64-
* Returns true if the property may be read.
64+
* Returns true if the property may be read from the store into the entity.
6565
*
6666
* @return true if readable, false otherwise
6767
* @since 4.0

src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
2626
import org.springframework.beans.BeanUtils;
27+
import org.springframework.data.annotation.ReadOnlyProperty;
2728
import org.springframework.data.domain.Range;
2829
import org.springframework.data.elasticsearch.annotations.DateFormat;
2930
import org.springframework.data.elasticsearch.annotations.Field;
@@ -119,7 +120,7 @@ public PropertyValueConverter getPropertyValueConverter() {
119120

120121
@Override
121122
public boolean isWritable() {
122-
return super.isWritable() && !isSeqNoPrimaryTermProperty();
123+
return !isTransient() && !isSeqNoPrimaryTermProperty() && !isAnnotationPresent(ReadOnlyProperty.class);
123124
}
124125

125126
@Override

src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java

+15-30
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,20 @@
1515
*/
1616
package org.springframework.data.elasticsearch.core;
1717

18-
import static java.util.Collections.singletonList;
19-
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21-
import static org.assertj.core.api.Assertions.fail;
22-
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
23-
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
24-
import static org.springframework.data.elasticsearch.annotations.Document.VersionType.EXTERNAL_GTE;
18+
import static java.util.Collections.*;
19+
import static org.assertj.core.api.Assertions.*;
20+
import static org.elasticsearch.index.query.QueryBuilders.*;
21+
import static org.springframework.data.elasticsearch.annotations.Document.VersionType.*;
22+
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
2523
import static org.springframework.data.elasticsearch.annotations.FieldType.Integer;
26-
import static org.springframework.data.elasticsearch.annotations.FieldType.Keyword;
27-
import static org.springframework.data.elasticsearch.annotations.FieldType.Text;
28-
import static org.springframework.data.elasticsearch.core.document.Document.create;
29-
import static org.springframework.data.elasticsearch.core.document.Document.parse;
30-
import static org.springframework.data.elasticsearch.utils.IdGenerator.nextIdAsString;
31-
import static org.springframework.data.elasticsearch.utils.IndexBuilder.buildIndex;
32-
24+
import static org.springframework.data.elasticsearch.core.document.Document.*;
25+
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
26+
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
27+
28+
import java.lang.Double;
29+
import java.lang.Integer;
30+
import java.lang.Long;
31+
import java.lang.Object;
3332
import java.util.ArrayList;
3433
import java.util.Arrays;
3534
import java.util.Collection;
@@ -55,7 +54,6 @@
5554
import org.springframework.dao.OptimisticLockingFailureException;
5655
import org.springframework.data.annotation.AccessType;
5756
import org.springframework.data.annotation.Id;
58-
import org.springframework.data.annotation.ReadOnlyProperty;
5957
import org.springframework.data.annotation.Version;
6058
import org.springframework.data.domain.PageRequest;
6159
import org.springframework.data.domain.Pageable;
@@ -79,20 +77,7 @@
7977
import org.springframework.data.elasticsearch.core.index.Settings;
8078
import org.springframework.data.elasticsearch.core.join.JoinField;
8179
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
82-
import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder;
83-
import org.springframework.data.elasticsearch.core.query.Criteria;
84-
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
85-
import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder;
86-
import org.springframework.data.elasticsearch.core.query.HighlightQuery;
87-
import org.springframework.data.elasticsearch.core.query.IndexQuery;
88-
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
89-
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
90-
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
91-
import org.springframework.data.elasticsearch.core.query.Query;
92-
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
93-
import org.springframework.data.elasticsearch.core.query.SourceFilter;
94-
import org.springframework.data.elasticsearch.core.query.StringQuery;
95-
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
80+
import org.springframework.data.elasticsearch.core.query.*;
9681
import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
9782
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
9883
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
@@ -4582,7 +4567,7 @@ static class ReadonlyIdEntity {
45824567
@Field(type = FieldType.Keyword) private String part2;
45834568

45844569
@Id
4585-
@ReadOnlyProperty
4570+
@WriteOnlyProperty
45864571
@AccessType(AccessType.Type.PROPERTY)
45874572
public String getId() {
45884573
return part1 + '-' + part2;

src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
import org.springframework.dao.OptimisticLockingFailureException;
5353
import org.springframework.data.annotation.AccessType;
5454
import org.springframework.data.annotation.Id;
55-
import org.springframework.data.annotation.ReadOnlyProperty;
5655
import org.springframework.data.annotation.Version;
5756
import org.springframework.data.domain.PageRequest;
5857
import org.springframework.data.domain.Pageable;
@@ -63,6 +62,7 @@
6362
import org.springframework.data.elasticsearch.annotations.FieldType;
6463
import org.springframework.data.elasticsearch.annotations.Mapping;
6564
import org.springframework.data.elasticsearch.annotations.Setting;
65+
import org.springframework.data.elasticsearch.annotations.WriteOnlyProperty;
6666
import org.springframework.data.elasticsearch.client.erhlc.NativeSearchQuery;
6767
import org.springframework.data.elasticsearch.client.erhlc.NativeSearchQueryBuilder;
6868
import org.springframework.data.elasticsearch.client.erhlc.ReactiveElasticsearchTemplate;
@@ -1506,7 +1506,7 @@ static class ReadonlyIdEntity {
15061506
@Field(type = FieldType.Keyword) private String part2;
15071507

15081508
@Id
1509-
@ReadOnlyProperty
1509+
@WriteOnlyProperty
15101510
@AccessType(AccessType.Type.PROPERTY)
15111511
public String getId() {
15121512
return part1 + '-' + part2;

src/test/java/org/springframework/data/elasticsearch/core/RuntimeFieldsIntegrationTests.java

+66-5
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,24 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.time.LocalDate;
21+
2022
import org.junit.jupiter.api.BeforeEach;
2123
import org.junit.jupiter.api.DisplayName;
2224
import org.junit.jupiter.api.Order;
2325
import org.junit.jupiter.api.Test;
2426
import org.springframework.beans.factory.annotation.Autowired;
2527
import org.springframework.data.annotation.Id;
26-
import org.springframework.data.elasticsearch.NewElasticsearchClientDevelopment;
28+
import org.springframework.data.annotation.ReadOnlyProperty;
29+
import org.springframework.data.elasticsearch.annotations.DateFormat;
2730
import org.springframework.data.elasticsearch.annotations.Document;
2831
import org.springframework.data.elasticsearch.annotations.Field;
2932
import org.springframework.data.elasticsearch.annotations.FieldType;
33+
import org.springframework.data.elasticsearch.annotations.Mapping;
3034
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
3135
import org.springframework.data.elasticsearch.core.query.Criteria;
3236
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
37+
import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder;
3338
import org.springframework.data.elasticsearch.core.query.Query;
3439
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
3540
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@@ -44,14 +49,13 @@ public abstract class RuntimeFieldsIntegrationTests {
4449

4550
@Autowired private ElasticsearchOperations operations;
4651
@Autowired protected IndexNameProvider indexNameProvider;
47-
private IndexOperations indexOperations;
4852

4953
@BeforeEach
5054
void setUp() {
5155

5256
indexNameProvider.increment();
53-
indexOperations = operations.indexOps(SomethingToBuy.class);
54-
indexOperations.createWithMapping();
57+
operations.indexOps(SomethingToBuy.class).createWithMapping();
58+
operations.indexOps(Person.class).createWithMapping();
5559
}
5660

5761
@Test
@@ -91,6 +95,24 @@ void shouldUseRuntimeFieldWithoutScript() {
9195
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("1");
9296
}
9397

98+
@Test // #2431
99+
@DisplayName("should return value from runtime field defined in mapping")
100+
void shouldReturnValueFromRuntimeFieldDefinedInMapping() {
101+
102+
var person = new Person();
103+
var years = 10;
104+
person.setBirthDate(LocalDate.now().minusDays(years * 365 + 100));
105+
operations.save(person);
106+
var query = Query.findAll();
107+
query.addFields("age");
108+
query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build());
109+
110+
var searchHits = operations.search(query, Person.class);
111+
112+
assertThat(searchHits.getTotalHits()).isEqualTo(1);
113+
assertThat(searchHits.getSearchHit(0).getContent().getAge()).isEqualTo(years);
114+
}
115+
94116
private void insert(String id, String description, double price) {
95117
SomethingToBuy entity = new SomethingToBuy();
96118
entity.setId(id);
@@ -99,7 +121,7 @@ private void insert(String id, String description, double price) {
99121
operations.save(entity);
100122
}
101123

102-
@Document(indexName = "#{@indexNameProvider.indexName()}")
124+
@Document(indexName = "#{@indexNameProvider.indexName()}-something")
103125
private static class SomethingToBuy {
104126
private @Id @Nullable String id;
105127

@@ -136,4 +158,43 @@ public void setPrice(@Nullable Double price) {
136158
this.price = price;
137159
}
138160
}
161+
162+
@Document(indexName = "#{@indexNameProvider.indexName()}-person")
163+
@Mapping(runtimeFieldsPath = "/runtime-fields-person.json")
164+
public class Person {
165+
@Nullable private String id;
166+
167+
@Field(type = FieldType.Date, format = DateFormat.basic_date)
168+
@Nullable private LocalDate birthDate;
169+
170+
@ReadOnlyProperty // do not write to prevent ES from automapping
171+
@Nullable private Integer age;
172+
173+
@Nullable
174+
public String getId() {
175+
return id;
176+
}
177+
178+
public void setId(@Nullable String id) {
179+
this.id = id;
180+
}
181+
182+
@Nullable
183+
public LocalDate getBirthDate() {
184+
return birthDate;
185+
}
186+
187+
public void setBirthDate(@Nullable LocalDate birthDate) {
188+
this.birthDate = birthDate;
189+
}
190+
191+
@Nullable
192+
public Integer getAge() {
193+
return age;
194+
}
195+
196+
public void setAge(@Nullable Integer age) {
197+
this.age = age;
198+
}
199+
}
139200
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"age": {
3+
"type": "long",
4+
"script": {
5+
"lang": "painless",
6+
"source": "Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); Instant startDate = doc['birthDate'].value.toInstant(); emit(ChronoUnit.DAYS.between(startDate, currentDate) / 365);"
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)