mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-13 07:32:11 +00:00
Enable non-field-backed properties.
Original Pull Request #2319 Closes #1489
This commit is contained in:
parent
5a52d6136f
commit
cdb92f6ee4
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user