mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-05-30 16:52:11 +00:00
CriteriaQuery must support nested queries.
Original Pull Request: #1757 Closes #1753
This commit is contained in:
parent
4ad002746e
commit
2bd4ef75cf
@ -24,6 +24,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil;
|
||||
import org.apache.lucene.search.join.ScoreMode;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
@ -136,7 +137,7 @@ class CriteriaQueryProcessor {
|
||||
return null;
|
||||
|
||||
String fieldName = field.getName();
|
||||
Assert.notNull(fieldName, "Unknown field");
|
||||
Assert.notNull(fieldName, "Unknown field " + fieldName);
|
||||
|
||||
Iterator<Criteria.CriteriaEntry> it = criteria.getQueryCriteriaEntries().iterator();
|
||||
QueryBuilder query;
|
||||
@ -152,6 +153,14 @@ class CriteriaQueryProcessor {
|
||||
}
|
||||
|
||||
addBoost(query, criteria.getBoost());
|
||||
|
||||
int dotPosition = fieldName.lastIndexOf('.');
|
||||
|
||||
if (dotPosition > 0) {
|
||||
String nestedPath = fieldName.substring(0, dotPosition);
|
||||
query = nestedQuery(nestedPath, query, ScoreMode.Avg);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
@ -168,8 +168,15 @@ public class MappingElasticsearchConverter
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <R> R read(Class<R> type, Document source) {
|
||||
|
||||
TypeInformation<R> typeHint = ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type));
|
||||
return read(typeHint, source);
|
||||
R r = read(typeHint, source);
|
||||
|
||||
if (r == null) {
|
||||
throw new ConversionException("could not convert into object of class " + type);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
|
||||
@ -188,7 +195,7 @@ public class MappingElasticsearchConverter
|
||||
|
||||
EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);
|
||||
|
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider);
|
||||
|
||||
if (!targetEntity.requiresPropertyPopulation()) {
|
||||
@ -246,6 +253,7 @@ public class MappingElasticsearchConverter
|
||||
ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator);
|
||||
|
||||
// TODO: Support for non-static inner classes via ObjectPath
|
||||
// noinspection ConstantConditions
|
||||
PersistentEntityParameterValueProvider<ElasticsearchPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
|
||||
entity, provider, null);
|
||||
|
||||
@ -281,7 +289,6 @@ public class MappingElasticsearchConverter
|
||||
return accessor.getBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
protected <R> R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, TypeInformation<?> type) {
|
||||
|
||||
@ -349,7 +356,7 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
|
||||
if (typeToUse.isMap()) {
|
||||
return (R) readMap(typeToUse, source);
|
||||
return readMap(typeToUse, source);
|
||||
}
|
||||
|
||||
if (typeToUse.equals(ClassTypeInformation.OBJECT)) {
|
||||
@ -549,7 +556,7 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
|
||||
Class<?> entityType = ClassUtils.getUserClass(source.getClass());
|
||||
TypeInformation<? extends Object> typeInformation = ClassTypeInformation.from(entityType);
|
||||
TypeInformation<?> typeInformation = ClassTypeInformation.from(entityType);
|
||||
|
||||
if (requiresTypeHint(entityType)) {
|
||||
typeMapper.writeType(typeInformation, sink);
|
||||
@ -561,9 +568,9 @@ public class MappingElasticsearchConverter
|
||||
/**
|
||||
* Internal write conversion method which should be used for nested invocations.
|
||||
*
|
||||
* @param source
|
||||
* @param sink
|
||||
* @param typeInformation
|
||||
* @param source the object to write
|
||||
* @param sink the write destination
|
||||
* @param typeInformation type information for the source
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
|
||||
@ -578,7 +585,10 @@ public class MappingElasticsearchConverter
|
||||
|
||||
if (customTarget.isPresent()) {
|
||||
Map<String, Object> result = conversionService.convert(source, Map.class);
|
||||
sink.putAll(result);
|
||||
|
||||
if (result != null) {
|
||||
sink.putAll(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -600,9 +610,9 @@ public class MappingElasticsearchConverter
|
||||
/**
|
||||
* Internal write conversion method which should be used for nested invocations.
|
||||
*
|
||||
* @param source
|
||||
* @param sink
|
||||
* @param entity
|
||||
* @param source the object to write
|
||||
* @param sink the write destination
|
||||
* @param entity entity for the source
|
||||
*/
|
||||
protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
|
||||
@Nullable ElasticsearchPersistentEntity<?> entity) {
|
||||
@ -734,7 +744,6 @@ public class MappingElasticsearchConverter
|
||||
*
|
||||
* @param collection must not be {@literal null}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected List<Object> createCollection(Collection<?> collection, ElasticsearchPersistentProperty property) {
|
||||
return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>(collection.size()));
|
||||
@ -745,7 +754,6 @@ public class MappingElasticsearchConverter
|
||||
*
|
||||
* @param map must not {@literal null}.
|
||||
* @param property must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected Map<String, Object> createMap(Map<?, ?> map, ElasticsearchPersistentProperty property) {
|
||||
|
||||
@ -761,7 +769,6 @@ public class MappingElasticsearchConverter
|
||||
* @param source must not be {@literal null}.
|
||||
* @param sink must not be {@literal null}.
|
||||
* @param propertyType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
protected Map<String, Object> writeMapInternal(Map<?, ?> source, Map<String, Object> sink,
|
||||
TypeInformation<?> propertyType) {
|
||||
@ -801,7 +808,6 @@ public class MappingElasticsearchConverter
|
||||
* @param source the collection to create a {@link Collection} for, must not be {@literal null}.
|
||||
* @param type the {@link TypeInformation} to consider or {@literal null} if unknown.
|
||||
* @param sink the {@link Collection} to write to.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Object> writeCollectionInternal(Collection<?> source, @Nullable TypeInformation<?> type,
|
||||
@ -837,8 +843,7 @@ public class MappingElasticsearchConverter
|
||||
/**
|
||||
* Returns a {@link String} representation of the given {@link Map} key
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
* @param key the key to convert
|
||||
*/
|
||||
private String potentiallyConvertMapKey(Object key) {
|
||||
|
||||
@ -846,17 +851,22 @@ public class MappingElasticsearchConverter
|
||||
return (String) key;
|
||||
}
|
||||
|
||||
return conversions.hasCustomWriteTarget(key.getClass(), String.class)
|
||||
? (String) getPotentiallyConvertedSimpleWrite(key, Object.class)
|
||||
: key.toString();
|
||||
if (conversions.hasCustomWriteTarget(key.getClass(), String.class)) {
|
||||
Object potentiallyConvertedSimpleWrite = getPotentiallyConvertedSimpleWrite(key, Object.class);
|
||||
|
||||
if (potentiallyConvertedSimpleWrite == null) {
|
||||
return key.toString();
|
||||
}
|
||||
return (String) potentiallyConvertedSimpleWrite;
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple Elasticsearch
|
||||
* type. Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
* @param value value to convert
|
||||
*/
|
||||
@Nullable
|
||||
private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class<?> typeHint) {
|
||||
@ -869,6 +879,10 @@ public class MappingElasticsearchConverter
|
||||
|
||||
if (conversionService.canConvert(value.getClass(), typeHint)) {
|
||||
value = conversionService.convert(value, typeHint);
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -908,8 +922,8 @@ public class MappingElasticsearchConverter
|
||||
* @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation<?> typeHint,
|
||||
Object value) {
|
||||
protected Object getWriteComplexValue(ElasticsearchPersistentProperty property,
|
||||
@SuppressWarnings("unused") TypeInformation<?> typeHint, Object value) {
|
||||
|
||||
Document document = Document.create();
|
||||
writeInternal(value, document, property.getTypeInformation());
|
||||
@ -928,11 +942,19 @@ public class MappingElasticsearchConverter
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param sink must not be {@literal null}.
|
||||
* @param type
|
||||
* @param type type to compare to
|
||||
*/
|
||||
protected void addCustomTypeKeyIfNecessary(Object source, Map<String, Object> sink, @Nullable TypeInformation<?> type) {
|
||||
protected void addCustomTypeKeyIfNecessary(Object source, Map<String, Object> sink,
|
||||
@Nullable TypeInformation<?> type) {
|
||||
|
||||
Class<?> reference = type != null ? type.getActualType().getType() : Object.class;
|
||||
Class<?> reference;
|
||||
|
||||
if (type == null) {
|
||||
reference = Object.class;
|
||||
} else {
|
||||
TypeInformation<?> actualType = type.getActualType();
|
||||
reference = actualType == null ? Object.class : actualType.getType();
|
||||
}
|
||||
Class<?> valueType = ClassUtils.getUserClass(source.getClass());
|
||||
|
||||
boolean notTheSameClass = !valueType.equals(reference);
|
||||
@ -987,8 +1009,7 @@ public class MappingElasticsearchConverter
|
||||
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
|
||||
* collection for everything else.
|
||||
*
|
||||
* @param source
|
||||
* @return
|
||||
* @param source object to convert
|
||||
*/
|
||||
private static Collection<?> asCollection(Object source) {
|
||||
|
||||
@ -1019,21 +1040,42 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
|
||||
private void updateCriteria(Criteria criteria, ElasticsearchPersistentEntity<?> persistentEntity) {
|
||||
|
||||
Field field = criteria.getField();
|
||||
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = field.getName();
|
||||
ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name);
|
||||
String[] fieldNames = field.getName().split("\\.");
|
||||
ElasticsearchPersistentEntity<?> currentEntity = persistentEntity;
|
||||
ElasticsearchPersistentProperty persistentProperty = null;
|
||||
for (int i = 0; i < fieldNames.length; i++) {
|
||||
persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]);
|
||||
|
||||
if (property != null && property.getName().equals(name)) {
|
||||
field.setName(property.getFieldName());
|
||||
if (persistentProperty != null) {
|
||||
fieldNames[i] = persistentProperty.getFieldName();
|
||||
try {
|
||||
currentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType());
|
||||
} catch (Exception e) {
|
||||
// using system types like UUIDs will lead to java.lang.reflect.InaccessibleObjectException in JDK 16
|
||||
// so if we cannot get an entity here, bail out.
|
||||
currentEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (property.hasPropertyConverter()) {
|
||||
if (currentEntity == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
field.setName(String.join(".", fieldNames));
|
||||
|
||||
if (persistentProperty != null) {
|
||||
|
||||
if (persistentProperty.hasPropertyConverter()) {
|
||||
ElasticsearchPersistentPropertyConverter propertyConverter = Objects
|
||||
.requireNonNull(property.getPropertyConverter());
|
||||
.requireNonNull(persistentProperty.getPropertyConverter());
|
||||
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
|
||||
Object value = criteriaEntry.getValue();
|
||||
if (value.getClass().isArray()) {
|
||||
@ -1047,7 +1089,7 @@ public class MappingElasticsearchConverter
|
||||
});
|
||||
}
|
||||
|
||||
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = property
|
||||
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty
|
||||
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
|
||||
|
||||
if (fieldAnnotation != null) {
|
||||
@ -1055,6 +1097,7 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static class MapValueAccessor {
|
||||
@ -1148,7 +1191,6 @@ public class MappingElasticsearchConverter
|
||||
this.evaluator = evaluator;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) {
|
||||
|
||||
|
@ -15,12 +15,14 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.skyscreamer.jsonassert.JSONAssert.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.json.JSONException;
|
||||
@ -68,8 +70,7 @@ public class CriteriaQueryMappingUnitTests {
|
||||
// endregion
|
||||
|
||||
// region tests
|
||||
@Test
|
||||
// DATAES-716
|
||||
@Test // DATAES-716
|
||||
void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException {
|
||||
|
||||
// use POJO properties and types in the query building
|
||||
@ -111,8 +112,7 @@ public class CriteriaQueryMappingUnitTests {
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
// #1668
|
||||
@Test // #1668
|
||||
void shouldMapNamesAndConvertValuesInCriteriaQueryForSubCriteria() throws JSONException {
|
||||
|
||||
// use POJO properties and types in the query building
|
||||
@ -185,8 +185,7 @@ public class CriteriaQueryMappingUnitTests {
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
// #1668
|
||||
@Test // #1668
|
||||
void shouldMapNamesAndConvertValuesInCriteriaQueryForSubCriteriaWithDate() throws JSONException {
|
||||
// use POJO properties and types in the query building
|
||||
CriteriaQuery criteriaQuery = new CriteriaQuery( //
|
||||
@ -258,8 +257,7 @@ public class CriteriaQueryMappingUnitTests {
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
// DATAES-706
|
||||
@Test // DATAES-706
|
||||
void shouldMapNamesAndValuesInSubCriteriaQuery() throws JSONException {
|
||||
|
||||
CriteriaQuery criteriaQuery = new CriteriaQuery( //
|
||||
@ -332,6 +330,41 @@ public class CriteriaQueryMappingUnitTests {
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
|
||||
@Test // #1753
|
||||
@DisplayName("should map names and value in nested entities")
|
||||
void shouldMapNamesAndValueInNestedEntities() throws JSONException {
|
||||
|
||||
String expected = "{\n" + //
|
||||
" \"bool\": {\n" + //
|
||||
" \"must\": [\n" + //
|
||||
" {\n" + //
|
||||
" \"nested\": {\n" + //
|
||||
" \"query\": {\n" + //
|
||||
" \"query_string\": {\n" + //
|
||||
" \"query\": \"03.10.1999\",\n" + //
|
||||
" \"fields\": [\n" + //
|
||||
" \"per-sons.birth-date^1.0\"\n" + //
|
||||
" ]\n" + //
|
||||
" }\n" + //
|
||||
" },\n" + //
|
||||
" \"path\": \"per-sons\"\n" + //
|
||||
" }\n" + //
|
||||
" }\n" + //
|
||||
" ]\n" + //
|
||||
" }\n" + //
|
||||
"}\n"; //
|
||||
|
||||
CriteriaQuery criteriaQuery = new CriteriaQuery(
|
||||
new Criteria("persons.birthDate").is(LocalDate.of(1999, 10, 3))
|
||||
);
|
||||
mappingElasticsearchConverter.updateQuery(criteriaQuery, House.class);
|
||||
String queryString = new CriteriaQueryProcessor().createQuery(criteriaQuery.getCriteria()).toString();
|
||||
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region helper functions
|
||||
private String getBase64EncodedGeoShapeQuery(GeoJson<?> geoJson, String elasticFieldName, String relation) {
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(("{\"geo_shape\": {\""
|
||||
@ -347,7 +380,15 @@ public class CriteriaQueryMappingUnitTests {
|
||||
@Nullable @Field(name = "first-name") String firstName;
|
||||
@Nullable @Field(name = "last-name") String lastName;
|
||||
@Nullable @Field(name = "created-date", type = FieldType.Date, format = DateFormat.epoch_millis) Date createdDate;
|
||||
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate birthDate;
|
||||
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = {},
|
||||
pattern = "dd.MM.uuuu") LocalDate birthDate;
|
||||
}
|
||||
|
||||
static class House {
|
||||
@Nullable @Id String id;
|
||||
@Nullable
|
||||
@Field(name = "per-sons", type = FieldType.Nested)
|
||||
List<Person> persons;
|
||||
}
|
||||
|
||||
static class GeoShapeEntity {
|
||||
|
@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core;
|
||||
import static org.skyscreamer.jsonassert.JSONAssert.*;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.elasticsearch.core.query.Criteria;
|
||||
|
||||
@ -338,4 +339,35 @@ class CriteriaQueryProcessorUnitTests {
|
||||
|
||||
assertEquals(expected, query, false);
|
||||
}
|
||||
|
||||
@Test // #1753
|
||||
@DisplayName("should build nested query")
|
||||
void shouldBuildNestedQuery() throws JSONException {
|
||||
|
||||
String expected = "{\n" + //
|
||||
" \"bool\" : {\n" + //
|
||||
" \"must\" : [\n" + //
|
||||
" {\n" + //
|
||||
" \"nested\" : {\n" + //
|
||||
" \"query\" : {\n" + //
|
||||
" \"query_string\" : {\n" + //
|
||||
" \"query\" : \"murphy\",\n" + //
|
||||
" \"fields\" : [\n" + //
|
||||
" \"houses.inhabitants.lastName^1.0\"\n" + //
|
||||
" ]\n" + //
|
||||
" }\n" + //
|
||||
" },\n" + //
|
||||
" \"path\" : \"houses.inhabitants\"\n" + //
|
||||
" }\n" + //
|
||||
" }\n" + //
|
||||
" ]\n" + //
|
||||
" }\n" + //
|
||||
"}"; //
|
||||
|
||||
Criteria criteria = new Criteria("houses.inhabitants.lastName").is("murphy");
|
||||
|
||||
String query = queryProcessor.createQuery(criteria).toString();
|
||||
|
||||
assertEquals(expected, query, false);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user