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. | 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. | 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]] | [[elasticsearch.mapping.meta-model.rules]] | ||||||
| === Mapping Rules | === Mapping Rules | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ import org.springframework.core.annotation.AliasFor; | |||||||
|  * @author Sascha Woo |  * @author Sascha Woo | ||||||
|  */ |  */ | ||||||
| @Retention(RetentionPolicy.RUNTIME) | @Retention(RetentionPolicy.RUNTIME) | ||||||
| @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) | @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD }) | ||||||
| @Documented | @Documented | ||||||
| @Inherited | @Inherited | ||||||
| public @interface Field { | 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.GeoShapeField; | ||||||
| import org.springframework.data.elasticsearch.annotations.MultiField; | import org.springframework.data.elasticsearch.annotations.MultiField; | ||||||
| import org.springframework.data.elasticsearch.annotations.ValueConverter; | 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.DatePropertyValueConverter; | ||||||
| import org.springframework.data.elasticsearch.core.convert.DateRangePropertyValueConverter; | import org.springframework.data.elasticsearch.core.convert.DateRangePropertyValueConverter; | ||||||
| import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter; | import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter; | ||||||
| @ -123,7 +124,7 @@ public class SimpleElasticsearchPersistentProperty extends | |||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public boolean isReadable() { | 	public boolean isReadable() { | ||||||
| 		return !isTransient() && !isSeqNoPrimaryTermProperty(); | 		return !isTransient() && !isSeqNoPrimaryTermProperty() && !isAnnotationPresent(WriteOnlyProperty.class); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@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.MultiField; | ||||||
| import org.springframework.data.elasticsearch.annotations.ScriptedField; | import org.springframework.data.elasticsearch.annotations.ScriptedField; | ||||||
| import org.springframework.data.elasticsearch.annotations.Setting; | 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.client.erhlc.NativeSearchQueryBuilder; | ||||||
| import org.springframework.data.elasticsearch.core.document.Explanation; | import org.springframework.data.elasticsearch.core.document.Explanation; | ||||||
| import org.springframework.data.elasticsearch.core.geo.GeoPoint; | import org.springframework.data.elasticsearch.core.geo.GeoPoint; | ||||||
| @ -197,8 +198,7 @@ public abstract class ElasticsearchIntegrationTests { | |||||||
| 		SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate) | 		SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate) | ||||||
| 				.version(System.currentTimeMillis()).build(); | 				.version(System.currentTimeMillis()).build(); | ||||||
| 
 | 
 | ||||||
| 		assertThatThrownBy(() -> operations.update(sampleEntity)) | 		assertThatThrownBy(() -> operations.update(sampleEntity)).isInstanceOf(DataAccessException.class); | ||||||
| 				.isInstanceOf(DataAccessException.class); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| @ -1529,8 +1529,8 @@ public abstract class ElasticsearchIntegrationTests { | |||||||
| 		String messageAfterUpdate = "test message"; | 		String messageAfterUpdate = "test message"; | ||||||
| 		String originalTypeInfo = "some type"; | 		String originalTypeInfo = "some type"; | ||||||
| 
 | 
 | ||||||
| 		SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate).type(originalTypeInfo) | 		SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message(messageBeforeUpdate) | ||||||
| 				.version(System.currentTimeMillis()).build(); | 				.type(originalTypeInfo).version(System.currentTimeMillis()).build(); | ||||||
| 		operations.save(sampleEntity); | 		operations.save(sampleEntity); | ||||||
| 
 | 
 | ||||||
| 		// modify the entity | 		// modify the entity | ||||||
| @ -3741,6 +3741,26 @@ public abstract class ElasticsearchIntegrationTests { | |||||||
| 		assertThat(readEntity.getPart2()).isEqualTo(entity.getPart2()); | 		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()}") | 	@Document(indexName = "#{@indexNameProvider.indexName()}") | ||||||
| 	private static class SampleEntityUUIDKeyed { | 	private static class SampleEntityUUIDKeyed { | ||||||
| 		@Nullable | 		@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 | 	// endregion | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user