mirror of
				https://github.com/spring-projects/spring-data-elasticsearch.git
				synced 2025-10-30 22:28:47 +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