Enable non-field-backed properties.

Original Pull Request #2319
Closes #1489
This commit is contained in:
Peter-Josef Meisch 2022-10-01 18:02:29 +02:00 committed by GitHub
parent 5a52d6136f
commit cdb92f6ee4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 6 deletions

View File

@ -159,6 +159,27 @@ It is also possible to define a `FieldNamingStrategy` in the configuration of th
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.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:
====
[source,java]
----
@Field(type = Keyword)
@WriteOnlyProperty
@AccessType(AccessType.Type.PROPERTY)
public String getProperty() {
return "some value that is calculated here";
}
----
====
[[elasticsearch.mapping.meta-model.rules]]
=== Mapping Rules

View File

@ -39,7 +39,7 @@ import org.springframework.core.annotation.AliasFor;
* @author Sascha Woo
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Documented
@Inherited
public @interface Field {

View File

@ -0,0 +1,35 @@
/*
* Copyright 2022 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 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 property that will be written to Elasticsearch, but not set when reading from Elasticsearch.
* This is needed for synthesized fields that may be used for search but that are not available in the entity.
*
* @author Peter-Josef Meisch
* @since 5.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Documented
public @interface WriteOnlyProperty {
}

View File

@ -32,6 +32,7 @@ import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.annotations.GeoShapeField;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.annotations.ValueConverter;
import org.springframework.data.elasticsearch.annotations.WriteOnlyProperty;
import org.springframework.data.elasticsearch.core.convert.DatePropertyValueConverter;
import org.springframework.data.elasticsearch.core.convert.DateRangePropertyValueConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter;
@ -123,7 +124,7 @@ public class SimpleElasticsearchPersistentProperty extends
@Override
public boolean isReadable() {
return !isTransient() && !isSeqNoPrimaryTermProperty();
return !isTransient() && !isSeqNoPrimaryTermProperty() && !isAnnotationPresent(WriteOnlyProperty.class);
}
@Override

View File

@ -68,6 +68,7 @@ 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;
@ -197,8 +198,7 @@ public abstract class ElasticsearchIntegrationTests {
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate)
.version(System.currentTimeMillis()).build();
assertThatThrownBy(() -> operations.update(sampleEntity))
.isInstanceOf(DataAccessException.class);
assertThatThrownBy(() -> operations.update(sampleEntity)).isInstanceOf(DataAccessException.class);
}
@Test
@ -1529,8 +1529,8 @@ public abstract class ElasticsearchIntegrationTests {
String messageAfterUpdate = "test message";
String originalTypeInfo = "some type";
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate).type(originalTypeInfo)
.version(System.currentTimeMillis()).build();
SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate)
.type(originalTypeInfo).version(System.currentTimeMillis()).build();
operations.save(sampleEntity);
// modify the entity
@ -3741,6 +3741,26 @@ public abstract class ElasticsearchIntegrationTests {
assertThat(readEntity.getPart2()).isEqualTo(entity.getPart2());
}
@Test // #1489
@DisplayName("should handle non-field-backed properties")
void shouldHandleNonFieldBackedProperties() {
operations.indexOps(NonFieldBackedPropertyClass.class).createWithMapping();
var entity = new NonFieldBackedPropertyClass();
entity.setId("007");
entity.setFirstName("James");
entity.setLastName("Bond");
operations.save(entity);
var searchHits = operations.search(new CriteriaQuery(Criteria.where("fullName").is("jamesbond")),
NonFieldBackedPropertyClass.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
assertThat(searchHits.getSearchHit(0).getContent()).isEqualTo(entity);
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
private static class SampleEntityUUIDKeyed {
@Nullable
@ -4517,5 +4537,78 @@ public abstract class ElasticsearchIntegrationTests {
}
}
@Document(indexName = "#{@indexNameProvider.indexName()}-readonly-id")
static class NonFieldBackedPropertyClass {
@Id
@Nullable private String id;
@Nullable
@Field(type = Text) private String firstName;
@Nullable
@Field(type = Text) private String lastName;
@Field(type = Keyword)
@WriteOnlyProperty
@AccessType(AccessType.Type.PROPERTY)
public String getFullName() {
return sanitize(firstName) + sanitize(lastName);
}
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getFirstName() {
return firstName;
}
public void setFirstName(@Nullable String firstName) {
this.firstName = firstName;
}
@Nullable
public String getLastName() {
return lastName;
}
public void setLastName(@Nullable String lastName) {
this.lastName = lastName;
}
private String sanitize(@Nullable String s) {
return s == null ? "" : s.replaceAll("\\s", "").toLowerCase();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
NonFieldBackedPropertyClass that = (NonFieldBackedPropertyClass) o;
if (!Objects.equals(id, that.id))
return false;
if (!Objects.equals(firstName, that.firstName))
return false;
return Objects.equals(lastName, that.lastName);
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (firstName != null ? firstName.hashCode() : 0);
result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
return result;
}
}
// endregion
}