From 0b36a268b21075a71a4a3447c864fa3b968e6356 Mon Sep 17 00:00:00 2001 From: Ezequiel Antunez Date: Thu, 5 Jan 2023 15:05:06 +0100 Subject: [PATCH 1/2] Add Query by Example feature #2418 Added Query by Example repositories fragments Added Criteria.regexp --- .../client/elc/CriteriaQueryProcessor.java | 10 +- .../client/erhlc/CriteriaQueryProcessor.java | 6 +- .../elasticsearch/core/query/Criteria.java | 20 +- .../ElasticsearchRepositoryFactory.java | 30 +- ...eactiveElasticsearchRepositoryFactory.java | 19 +- .../querybyexample/ExampleCriteriaMapper.java | 174 +++++++ .../QueryByExampleElasticsearchExecutor.java | 108 +++++ ...veQueryByExampleElasticsearchExecutor.java | 99 ++++ .../elc/CriteriaQueryProcessorUnitTests.java | 29 +- .../CriteriaQueryProcessorUnitTests.java | 28 +- ...sticsearchExecutorELCIntegrationTests.java | 44 ++ ...icsearchExecutorERHLCIntegrationTests.java | 44 ++ ...ElasticsearchExecutorIntegrationTests.java | 458 ++++++++++++++++++ ...sticsearchExecutorELCIntegrationTests.java | 43 ++ ...icsearchExecutorERHLCIntegrationTests.java | 43 ++ ...ElasticsearchExecutorIntegrationTests.java | 413 ++++++++++++++++ 16 files changed, 1555 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java index bb0af7deb..0639eddfd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -39,6 +39,7 @@ * query. * * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho * @since 4.4 */ class CriteriaQueryProcessor { @@ -329,6 +330,13 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable"); } break; + case REGEXP: + queryBuilder // + .regexp(rb -> rb // + .field(fieldName) // + .value(searchText) // + .boost(boost)); // + break; default: throw new CriteriaQueryException("Could not build query for " + entry); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java index 513c19253..e3155edba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2023 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. @@ -44,6 +44,7 @@ * @author Rasmus Faber-Espensen * @author James Bodkin * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho * @deprecated since 5.0 */ @Deprecated @@ -248,6 +249,9 @@ private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) { } } break; + case REGEXP: + query = regexpQuery(fieldName, searchText); + break; } return query; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index 535983eeb..cffe0aa6c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2023 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. @@ -50,6 +50,7 @@ * @author Mohsin Husen * @author Franck Marchand * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ public class Criteria { @@ -611,6 +612,20 @@ public Criteria notEmpty() { return this; } + /** + * Add a {@link OperationKey#REGEXP} entry to the {@link #queryCriteriaEntries}. + * + * @param value the regexp value to match + * @return this object + */ + public Criteria regexp(Object value) { + + Assert.notNull(value, "value must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.REGEXP, value)); + return this; + } + // endregion // region criteria entries - filter @@ -954,7 +969,8 @@ public enum OperationKey { // /** * @since 4.3 */ - NOT_EMPTY; + NOT_EMPTY, // + REGEXP; /** * @return true if this key does not have an associated value diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java index 02d4377e8..a8e56c053 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2023 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. @@ -15,22 +15,21 @@ */ package org.springframework.data.elasticsearch.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - -import java.lang.reflect.Method; -import java.util.Optional; - import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery; import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery; +import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -38,6 +37,11 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT; + /** * Factory to create {@link ElasticsearchRepository} * @@ -49,6 +53,7 @@ * @author Christoph Strobl * @author Sascha Woo * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { @@ -122,4 +127,17 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { return new ElasticsearchRepositoryMetadata(repositoryInterface); } + + @Override + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty(); + + if (QueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.implemented(QueryByExampleExecutor.class, + instantiateClass(QueryByExampleElasticsearchExecutor.class, elasticsearchOperations))); + } + + return fragments; + } + } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java index d079c8e45..226f2e2c9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2023 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. @@ -25,15 +25,19 @@ import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery; import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery; +import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; @@ -45,6 +49,7 @@ * * @author Christoph Strobl * @author Ivan Greene + * @author Ezequiel Antúnez Camacho * @since 3.2 */ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -168,4 +173,16 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, } } } + + @Override + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty(); + + if (ReactiveQueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.implemented(ReactiveQueryByExampleExecutor.class, + instantiateClass(ReactiveQueryByExampleElasticsearchExecutor.class, operations))); + } + + return fragments; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java new file mode 100644 index 000000000..b7c788465 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java @@ -0,0 +1,174 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.support.ExampleMatcherAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Maps a {@link Example} to a {@link org.springframework.data.elasticsearch.core.query.Criteria} + * + * @param Class type + * @author Ezequiel Antúnez Camacho + */ +class ExampleCriteriaMapper { + + private static final Set SUPPORTED_MATCHERS = EnumSet.of( + ExampleMatcher.StringMatcher.DEFAULT, ExampleMatcher.StringMatcher.EXACT, ExampleMatcher.StringMatcher.STARTING, + ExampleMatcher.StringMatcher.CONTAINING, ExampleMatcher.StringMatcher.ENDING); + + private final MappingContext, ElasticsearchPersistentProperty> mappingContext; + + /** + * Builds a {@link ExampleCriteriaMapper} + * + * @param mappingContext mappingContext to use + */ + ExampleCriteriaMapper( + MappingContext, ElasticsearchPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + Criteria criteria(Example example) { + return buildCriteria(example); + } + + private Criteria buildCriteria(Example example) { + final ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher()); + + return applyPropertySpecs(new Criteria(), "", example.getProbe(), + mappingContext.getRequiredPersistentEntity(example.getProbeType()), matcherAccessor, + example.getMatcher().getMatchMode()); + } + + private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Object probe, + ElasticsearchPersistentEntity persistentEntity, ExampleMatcherAccessor exampleSpecAccessor, + ExampleMatcher.MatchMode matchMode) { + + if (probe == null) { + return criteria; + } + + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(probe); + + for (ElasticsearchPersistentProperty property : persistentEntity) { + final String propertyName = getPropertyName(property); + String propertyPath = StringUtils.hasText(path) ? (path + "." + propertyName) : propertyName; + if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike() + || property.isVersionProperty()) { + continue; + } + + Object propertyValue = propertyAccessor.getProperty(property); + if (property.isMap() && propertyValue != null) { + for (Map.Entry entry : ((Map) propertyValue).entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + criteria = applyPropertySpec(propertyPath + "." + key, value, exampleSpecAccessor, property, matchMode, + criteria); + } + continue; + } + + criteria = applyPropertySpec(propertyPath, propertyValue, exampleSpecAccessor, property, matchMode, criteria); + } + return criteria; + } + + private String getPropertyName(ElasticsearchPersistentProperty property) { + return property.isIdProperty() ? "_id" : property.getName(); + } + + private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor, + ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) { + + if (exampleSpecAccessor.isIgnoreCaseForPath(path)) { + throw new InvalidDataAccessApiUsageException( + "Current implementation of Query-by-Example supports only case-sensitive matching."); + } + + ExampleMatcher.StringMatcher stringMatcher = exampleSpecAccessor.getStringMatcherForPath(path); + if (!SUPPORTED_MATCHERS.contains(exampleSpecAccessor.getStringMatcherForPath(path))) { + throw new InvalidDataAccessApiUsageException(String.format( + "Current implementation of Query-by-Example does not support string matcher %s. Supported matchers are: %s.", + stringMatcher, SUPPORTED_MATCHERS)); + } + + final Object transformedValue = exampleSpecAccessor.getValueTransformerForPath(path) + .apply(Optional.ofNullable(propertyValue)).orElse(null); + + if (transformedValue == null) { + criteria = tryToAppendMustNotSentence(criteria, path, exampleSpecAccessor); + } else { + if (property.isEntity()) { + return applyPropertySpecs(criteria, path, transformedValue, + mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor, matchMode); + } else { + return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue, stringMatcher); + } + } + return criteria; + } + + private Criteria tryToAppendMustNotSentence(Criteria criteria, String path, + ExampleMatcherAccessor exampleSpecAccessor) { + if (ExampleMatcher.NullHandler.INCLUDE.equals(exampleSpecAccessor.getNullHandler()) + || exampleSpecAccessor.hasPropertySpecifier(path)) { + return criteria.and(path).not().exists(); + } + return criteria; + } + + private Criteria applyMatchMode(Criteria criteria, String path, ExampleMatcher.MatchMode matchMode) { + if (matchMode == ExampleMatcher.MatchMode.ALL) { + return criteria.and(path); + } else { + return criteria.or(path); + } + } + + private Criteria applyStringMatcher(Criteria criteria, Object value, ExampleMatcher.StringMatcher stringMatcher) { + return switch (stringMatcher) { + case DEFAULT, EXACT -> criteria.is(value); + case STARTING -> criteria.startsWith(validateString(value)); + case ENDING -> criteria.endsWith(validateString(value)); + case CONTAINING -> criteria.contains(validateString(value)); + case REGEX -> throw new UnsupportedOperationException("REGEX matcher is unsupported"); + }; + } + + private String validateString(Object value) { + if (value instanceof String) { + return value.toString(); + } + throw new IllegalArgumentException("This operation requires a String but got " + value.getClass()); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java new file mode 100644 index 000000000..9bd2d74c8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java @@ -0,0 +1,108 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHitSupport; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.SearchPage; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.QueryByExampleExecutor; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * @author Ezequiel Antúnez Camacho + */ +public class QueryByExampleElasticsearchExecutor implements QueryByExampleExecutor { + + protected ElasticsearchOperations operations; + protected ExampleCriteriaMapper exampleCriteriaMapper; + + public QueryByExampleElasticsearchExecutor(ElasticsearchOperations operations) { + this.operations = operations; + this.exampleCriteriaMapper = new ExampleCriteriaMapper<>( + (MappingContext, ElasticsearchPersistentProperty>) operations + .getElasticsearchConverter().getMappingContext()); + } + + @Override + public Optional findOne(Example example) { + CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + if (searchHits.getTotalHits() > 1) { + throw new org.springframework.dao.IncorrectResultSizeDataAccessException(1); + } + return Optional.ofNullable(searchHits).filter(SearchHits::hasSearchHits) + .map(result -> (List) SearchHitSupport.unwrapSearchHits(result)).map(s -> s.get(0)); + } + + @Override + public Iterable findAll(Example example) { + CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + return (List) SearchHitSupport.unwrapSearchHits(searchHits); + } + + @Override + public Iterable findAll(Example example, Sort sort) { + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withSort(sort).build(); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + return (List) SearchHitSupport.unwrapSearchHits(searchHits); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withPageable(pageable) + .build(); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + SearchPage page = SearchHitSupport.searchPageFor(searchHits, criteriaQuery.getPageable()); + return (Page) SearchHitSupport.unwrapSearchHits(page); + + } + + @Override + public long count(Example example) { + final CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + return operations.count(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + } + + @Override + public boolean exists(Example example) { + return count(example) > 0L; + } + + @Override + public R findBy(Example example, Function, R> queryFunction) { + throw new UnsupportedOperationException("findBy example and queryFunction is not supported"); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java new file mode 100644 index 000000000..3bdbdf0fe --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.reactivestreams.Publisher; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +/** + * @author Ezequiel Antúnez Camacho + */ +public class ReactiveQueryByExampleElasticsearchExecutor implements ReactiveQueryByExampleExecutor { + + protected ReactiveElasticsearchOperations operations; + protected ExampleCriteriaMapper exampleCriteriaMapper; + + public ReactiveQueryByExampleElasticsearchExecutor(ReactiveElasticsearchOperations operations) { + this.operations = operations; + this.exampleCriteriaMapper = new ExampleCriteriaMapper<>( + (MappingContext, ElasticsearchPersistentProperty>) operations + .getElasticsearchConverter().getMappingContext()); + } + + @Override + public Mono findOne(Example example) { + return Mono.just(example) + .map(e -> CriteriaQuery.builder(exampleCriteriaMapper.criteria(e)).withMaxResults(2).build()) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .buffer(2).map(searchHitList -> { + if (searchHitList.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + return searchHitList.iterator().next(); + }).map(SearchHit::getContent).next(); + } + + @Override + public Flux findAll(Example example) { + return Mono.just(example).map(e -> new CriteriaQuery(exampleCriteriaMapper.criteria(e))) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .map(SearchHit::getContent); + } + + @Override + public Flux findAll(Example example, Sort sort) { + return Mono.just(example).map(e -> CriteriaQuery.builder(exampleCriteriaMapper.criteria(e)).withSort(sort).build()) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .map(SearchHit::getContent); + } + + @Override + public Mono count(Example example) { + return Mono.just(example).map(e -> new CriteriaQuery(exampleCriteriaMapper.criteria(e))) + .flatMap(criteriaQuery -> operations.count(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))); + } + + @Override + public Mono exists(Example example) { + return count(example).map(count -> count > 0); + + } + + @Override + public > P findBy(Example example, + Function, P> queryFunction) { + throw new UnsupportedOperationException("findBy example and queryFunction is not supported"); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java index 5b662d5fe..e0c7f80d9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 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. @@ -28,6 +28,7 @@ /** * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ @SuppressWarnings("ConstantConditions") class CriteriaQueryProcessorUnitTests { @@ -456,4 +457,30 @@ void shouldBuildQueryForNonEmptyProperty() throws JSONException { assertEquals(expected, queryString, false); } + + @Test // #2418 + void shouldBuildRegexpQuery() throws JSONException { + String expected = """ + { + "bool": { + "must": [ + { + "regexp": { + "field1": { + "value": "\\\\[\\\\^abc\\\\]" + } + } + } + ] + } + } + """; + + Criteria criteria = new Criteria("field1").regexp("[^abc]"); + + var queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteria), mapper); + + assertEquals(expected, queryString, false); + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java index 4e5bca7ec..b1c910a79 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 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. @@ -24,6 +24,7 @@ /** * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ @SuppressWarnings("ConstantConditions") class CriteriaQueryProcessorUnitTests { @@ -447,4 +448,29 @@ void shouldBuildQueryForNonEmptyProperty() throws JSONException { assertEquals(expected, query, false); } + + @Test // #2418 + void shouldBuildRegexpQuery() throws JSONException { + String expected = """ + { + "bool": { + "must": [ + { + "regexp": { + "field1": { + "value": "\\\\[\\\\^abc\\\\]" + } + } + } + ] + } + } + """; + + Criteria criteria = new Criteria("field1").regexp("[^abc]"); + + String queryString = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, queryString, false); + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java new file mode 100644 index 000000000..87af236af --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + */ +@ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) +public class QueryByExampleElasticsearchExecutorELCIntegrationTests + extends QueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.repository.support" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("query-by-example-repository"); + } + + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java new file mode 100644 index 000000000..f182c64a1 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + */ +@ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) +public class QueryByExampleElasticsearchExecutorERHLCIntegrationTests + extends QueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ElasticsearchRestTemplateConfiguration.class }) + @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.repository.support" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("query-by-example-repository-es7"); + } + + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java new file mode 100644 index 000000000..1ab145a53 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java @@ -0,0 +1,458 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.assertj.core.api.AbstractThrowableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.lang.Nullable; + +import java.util.Objects; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.data.elasticsearch.utils.IdGenerator.nextIdAsString; + +/** + * @author Ezequiel Antúnez Camacho + */ +@SpringIntegrationTest +abstract class QueryByExampleElasticsearchExecutorIntegrationTests { + + @Autowired private SampleElasticsearchRepository repository; + @Autowired private ElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + operations.indexOps(SampleEntity.class).createWithMapping(); + } + + @Test // #2418 + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + @Test // #2418 + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + // when + SampleEntity probe = new SampleEntity(); + sampleEntity.setId(documentId); + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity); + + } + + @Test // #2418 + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity2); + + // when + SampleEntity probe = new SampleEntity(); + AbstractThrowableAssert assertThatThrownBy = assertThatThrownBy( + () -> repository.findOne(Example.of(probe))); + + // then + assertThatThrownBy.isInstanceOf(IncorrectResultSizeDataAccessException.class); + + } + + @Test // #2418 + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + // when + SampleEntity probe = new SampleEntity(); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity); + + } + + @Test // #2418 + void shouldFindAll() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity2); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world."); + Iterable sampleEntities = repository.findAll(Example.of(probe)); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void shouldFindAllWithMatchers() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity2); + + String documentId3 = nextIdAsString(); + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setId(documentId3); + sampleEntity3.setMessage("hola mundo."); + sampleEntity3.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity3); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void shouldFindAllWithSort() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setRate(1); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setRate(3); + sampleEntity2.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity2); + + String documentId3 = nextIdAsString(); + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setId(documentId3); + sampleEntity3.setMessage("hello world."); + sampleEntity3.setRate(2); + sampleEntity3.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity3); + + // when + SampleEntity probe = new SampleEntity(); + final Iterable all = repository.findAll(); + Iterable sampleEntities = repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")); + + // then + assertThat(sampleEntities).isNotNull().hasSize(3).containsExactly(sampleEntity2, sampleEntity3, sampleEntity); + } + + @Test // #2418 + void shouldFindAllWithPageable() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setRate(1); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setRate(3); + sampleEntity2.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity2); + + String documentId3 = nextIdAsString(); + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setId(documentId3); + sampleEntity3.setMessage("hello world."); + sampleEntity3.setRate(2); + sampleEntity3.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity3); + + // when + SampleEntity probe = new SampleEntity(); + Iterable page1 = repository.findAll(Example.of(probe), + PageRequest.of(0, 2, Sort.Direction.DESC, "rate")); + Iterable page2 = repository.findAll(Example.of(probe), + PageRequest.of(1, 2, Sort.Direction.DESC, "rate")); + + // then + assertThat(page1).isNotNull().hasSize(2).containsExactly(sampleEntity2, sampleEntity3); + assertThat(page2).isNotNull().hasSize(1).containsExactly(sampleEntity); + } + + @Test // #2418 + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + // when + Long count = repository.count(Example.of(sampleEntity)); + + // then + assertThat(count).isPositive(); + } + + @Test // #2418 + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity); + + // when + boolean exists = repository.exists(Example.of(sampleEntity)); + + // then + assertThat(exists).isTrue(); + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class SampleEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String type; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String message; + @Nullable private Integer rate; + @Nullable private Boolean available; + @Nullable + @Field(type = FieldType.Nested, store = true, fielddata = true) private SampleNestedEntity sampleNestedEntity; + @Nullable + @Version private Long version; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public Integer getRate() { + return rate; + } + + public void setRate(Integer rate) { + this.rate = rate; + } + + @Nullable + public Boolean isAvailable() { + return available; + } + + public void setAvailable(Boolean available) { + this.available = available; + } + + @Nullable + public SampleNestedEntity getSampleNestedEntity() { + return sampleNestedEntity; + } + + public void setSampleNestedEntity(SampleNestedEntity sampleNestedEntity) { + this.sampleNestedEntity = sampleNestedEntity; + } + + @Nullable + public java.lang.Long getVersion() { + return version; + } + + public void setVersion(@Nullable java.lang.Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleEntity that = (SampleEntity) o; + + if (rate != that.rate) + return false; + if (available != that.available) + return false; + if (!Objects.equals(id, that.id)) + return false; + if (!Objects.equals(type, that.type)) + return false; + if (!Objects.equals(message, that.message)) + return false; + if (!Objects.equals(sampleNestedEntity, that.sampleNestedEntity)) + return false; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + rate; + result = 31 * result + (available ? 1 : 0); + result = 31 * result + (sampleNestedEntity != null ? sampleNestedEntity.hashCode() : 0); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + + static class SampleNestedEntity { + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + + @Nullable + public String getNestedData() { + return nestedData; + } + + public void setNestedData(@Nullable String nestedData) { + this.nestedData = nestedData; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleNestedEntity that = (SampleNestedEntity) o; + + return Objects.equals(nestedData, that.nestedData); + } + + @Override + public int hashCode() { + return nestedData != null ? nestedData.hashCode() : 0; + } + } + } + + interface SampleElasticsearchRepository + extends ElasticsearchRepository, QueryByExampleExecutor {} + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java new file mode 100644 index 000000000..5063397e1 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + */ +@ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) +public class ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests + extends ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-query-by-example-repository"); + } + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java new file mode 100644 index 000000000..a25a38121 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + */ +@ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) +public class ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests + extends ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-query-by-example-repository-es7"); + } + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java new file mode 100644 index 000000000..62bbf8c72 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java @@ -0,0 +1,413 @@ +/* + * Copyright 2023 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.repository.support.querybyexample; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.lang.Nullable; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.elasticsearch.utils.IdGenerator.nextIdAsString; + +/** + * @author Ezequiel Antúnez Camacho + */ +@SpringIntegrationTest +abstract class ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Autowired private SampleReactiveElasticsearchRepository repository; + @Autowired private ReactiveElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + operations.indexOps(SampleEntity.class).createWithMapping(); + } + + @Test // #2418 + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + @Test + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + SampleEntity probe = new SampleEntity(); + sampleEntity.setId(documentId); + repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + // when + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) + .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity)) // + .verifyComplete(); + } + + @Test + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2)).as(StepVerifier::create).expectNextCount(2L) + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create).expectError(IncorrectResultSizeDataAccessException.class).verify(); + } + + @Test + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) + .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity)) // + .verifyComplete(); + } + + @Test + void shouldFindAll() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2)).as(StepVerifier::create).expectNextCount(2L) + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world."); + repository.findAll(Example.of(probe)) + // then + .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity, sampleEntity2)).verifyComplete(); + } + + @Test + void shouldFindAllWithMatchers() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("bye world."); + sampleEntity2.setVersion(System.currentTimeMillis()); + + String documentId3 = nextIdAsString(); + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setId(documentId3); + sampleEntity3.setMessage("hola mundo."); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2, sampleEntity3)).as(StepVerifier::create) + .expectNextCount(3L).verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)))) + // then + .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity, sampleEntity2)).verifyComplete(); + } + + @Test + void shouldFindAllWithSort() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("hello world."); + sampleEntity.setRate(1); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage("hello world."); + sampleEntity2.setRate(3); + sampleEntity2.setVersion(System.currentTimeMillis()); + + String documentId3 = nextIdAsString(); + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setId(documentId3); + sampleEntity3.setMessage("hello world."); + sampleEntity3.setRate(2); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2, sampleEntity3)).as(StepVerifier::create) + .expectNextCount(3L).verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")) + // then + .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity2, sampleEntity3, sampleEntity)) + .verifyComplete(); + ; + } + + @Test + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + + // when + repository.count(Example.of(sampleEntity)) + // then + .as(StepVerifier::create).expectNext(1L).verifyComplete(); + } + + @Test + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); + + // when + repository.exists(Example.of(sampleEntity)) + // then + .as(StepVerifier::create).expectNext(true).verifyComplete(); + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class SampleEntity { + @Nullable + @Id private String id; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String type; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String message; + @Nullable private Integer rate; + @Nullable private Boolean available; + @Nullable + @Field(type = FieldType.Nested, store = true, fielddata = true) private SampleNestedEntity sampleNestedEntity; + @Nullable + @Version private Long version; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public Integer getRate() { + return rate; + } + + public void setRate(Integer rate) { + this.rate = rate; + } + + @Nullable + public Boolean isAvailable() { + return available; + } + + public void setAvailable(Boolean available) { + this.available = available; + } + + @Nullable + public SampleNestedEntity getSampleNestedEntity() { + return sampleNestedEntity; + } + + public void setSampleNestedEntity(SampleNestedEntity sampleNestedEntity) { + this.sampleNestedEntity = sampleNestedEntity; + } + + @Nullable + public java.lang.Long getVersion() { + return version; + } + + public void setVersion(@Nullable java.lang.Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleEntity that = (SampleEntity) o; + + if (rate != that.rate) + return false; + if (available != that.available) + return false; + if (!Objects.equals(id, that.id)) + return false; + if (!Objects.equals(type, that.type)) + return false; + if (!Objects.equals(message, that.message)) + return false; + if (!Objects.equals(sampleNestedEntity, that.sampleNestedEntity)) + return false; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (rate != null ? rate.hashCode() : 0); + result = 31 * result + (available != null ? available.hashCode() : 0); + result = 31 * result + (sampleNestedEntity != null ? sampleNestedEntity.hashCode() : 0); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + + static class SampleNestedEntity { + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + + @Nullable + public String getNestedData() { + return nestedData; + } + + public void setNestedData(@Nullable String nestedData) { + this.nestedData = nestedData; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleNestedEntity that = (SampleNestedEntity) o; + + return Objects.equals(nestedData, that.nestedData); + } + + @Override + public int hashCode() { + return nestedData != null ? nestedData.hashCode() : 0; + } + } + } + + interface SampleReactiveElasticsearchRepository + extends ReactiveElasticsearchRepository, ReactiveQueryByExampleExecutor {} + +} From bbadfe99f1f8e835fc2731e0ae70188dfdf80b8b Mon Sep 17 00:00:00 2001 From: Ezequiel Antunez Date: Wed, 11 Jan 2023 13:35:57 +0100 Subject: [PATCH 2/2] Add Query by Example feature #2418 Fixed Criteria.regexp Minor fixes in QueryByExample fragments and mapper Improved test coverage --- .../client/elc/CriteriaQueryProcessor.java | 2 +- .../client/erhlc/CriteriaQueryProcessor.java | 2 +- .../elasticsearch/core/query/Criteria.java | 6 +- .../querybyexample/ExampleCriteriaMapper.java | 45 +- .../QueryByExampleElasticsearchExecutor.java | 20 +- ...veQueryByExampleElasticsearchExecutor.java | 19 +- .../elc/CriteriaQueryProcessorUnitTests.java | 2 +- .../CriteriaQueryProcessorUnitTests.java | 2 +- ...sticsearchExecutorELCIntegrationTests.java | 2 +- ...icsearchExecutorERHLCIntegrationTests.java | 2 +- ...ElasticsearchExecutorIntegrationTests.java | 656 +++++++++++------- ...sticsearchExecutorELCIntegrationTests.java | 5 +- ...icsearchExecutorERHLCIntegrationTests.java | 1 + ...ElasticsearchExecutorIntegrationTests.java | 641 +++++++++++------ 14 files changed, 904 insertions(+), 501 deletions(-) diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java index 0639eddfd..5e01e2f81 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java @@ -334,7 +334,7 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, queryBuilder // .regexp(rb -> rb // .field(fieldName) // - .value(searchText) // + .value(value.toString()) // .boost(boost)); // break; default: diff --git a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java index e3155edba..bd001c267 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java @@ -250,7 +250,7 @@ private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) { } break; case REGEXP: - query = regexpQuery(fieldName, searchText); + query = regexpQuery(fieldName, value.toString()); break; } return query; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index cffe0aa6c..0fab5c5e9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -617,8 +617,9 @@ public Criteria notEmpty() { * * @param value the regexp value to match * @return this object + * @since 5.1 */ - public Criteria regexp(Object value) { + public Criteria regexp(String value) { Assert.notNull(value, "value must not be null"); @@ -970,6 +971,9 @@ public enum OperationKey { // * @since 4.3 */ NOT_EMPTY, // + /** + * @since 5.1 + */ REGEXP; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java index b7c788465..45ad8d0ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java @@ -15,6 +15,9 @@ */ package org.springframework.data.elasticsearch.repository.support.querybyexample; +import java.util.Map; +import java.util.Optional; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; @@ -27,24 +30,15 @@ import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; -import java.util.EnumSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - /** * Maps a {@link Example} to a {@link org.springframework.data.elasticsearch.core.query.Criteria} * - * @param Class type * @author Ezequiel Antúnez Camacho + * @since 5.1 */ -class ExampleCriteriaMapper { +class ExampleCriteriaMapper { - private static final Set SUPPORTED_MATCHERS = EnumSet.of( - ExampleMatcher.StringMatcher.DEFAULT, ExampleMatcher.StringMatcher.EXACT, ExampleMatcher.StringMatcher.STARTING, - ExampleMatcher.StringMatcher.CONTAINING, ExampleMatcher.StringMatcher.ENDING); - - private final MappingContext, ElasticsearchPersistentProperty> mappingContext; + private final MappingContext, ElasticsearchPersistentProperty> mappingContext; /** * Builds a {@link ExampleCriteriaMapper} @@ -52,15 +46,15 @@ class ExampleCriteriaMapper { * @param mappingContext mappingContext to use */ ExampleCriteriaMapper( - MappingContext, ElasticsearchPersistentProperty> mappingContext) { + MappingContext, ElasticsearchPersistentProperty> mappingContext) { this.mappingContext = mappingContext; } - Criteria criteria(Example example) { + Criteria criteria(Example example) { return buildCriteria(example); } - private Criteria buildCriteria(Example example) { + private Criteria buildCriteria(Example example) { final ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher()); return applyPropertySpecs(new Criteria(), "", example.getProbe(), @@ -79,7 +73,7 @@ private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Ob PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(probe); for (ElasticsearchPersistentProperty property : persistentEntity) { - final String propertyName = getPropertyName(property); + final String propertyName = property.getName(); String propertyPath = StringUtils.hasText(path) ? (path + "." + propertyName) : propertyName; if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike() || property.isVersionProperty()) { @@ -102,10 +96,6 @@ private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Ob return criteria; } - private String getPropertyName(ElasticsearchPersistentProperty property) { - return property.isIdProperty() ? "_id" : property.getName(); - } - private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor, ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) { @@ -114,13 +104,6 @@ private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMat "Current implementation of Query-by-Example supports only case-sensitive matching."); } - ExampleMatcher.StringMatcher stringMatcher = exampleSpecAccessor.getStringMatcherForPath(path); - if (!SUPPORTED_MATCHERS.contains(exampleSpecAccessor.getStringMatcherForPath(path))) { - throw new InvalidDataAccessApiUsageException(String.format( - "Current implementation of Query-by-Example does not support string matcher %s. Supported matchers are: %s.", - stringMatcher, SUPPORTED_MATCHERS)); - } - final Object transformedValue = exampleSpecAccessor.getValueTransformerForPath(path) .apply(Optional.ofNullable(propertyValue)).orElse(null); @@ -131,7 +114,8 @@ private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMat return applyPropertySpecs(criteria, path, transformedValue, mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor, matchMode); } else { - return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue, stringMatcher); + return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue, + exampleSpecAccessor.getStringMatcherForPath(path)); } } return criteria; @@ -156,11 +140,12 @@ private Criteria applyMatchMode(Criteria criteria, String path, ExampleMatcher.M private Criteria applyStringMatcher(Criteria criteria, Object value, ExampleMatcher.StringMatcher stringMatcher) { return switch (stringMatcher) { - case DEFAULT, EXACT -> criteria.is(value); + case DEFAULT -> criteria.is(value); + case EXACT -> criteria.matchesAll(value); case STARTING -> criteria.startsWith(validateString(value)); case ENDING -> criteria.endsWith(validateString(value)); case CONTAINING -> criteria.contains(validateString(value)); - case REGEX -> throw new UnsupportedOperationException("REGEX matcher is unsupported"); + case REGEX -> criteria.regexp(validateString(value)); }; } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java index 9bd2d74c8..c64c8faf3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java @@ -15,6 +15,10 @@ */ package org.springframework.data.elasticsearch.repository.support.querybyexample; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -23,35 +27,27 @@ import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchPage; -import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; -import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ public class QueryByExampleElasticsearchExecutor implements QueryByExampleExecutor { protected ElasticsearchOperations operations; - protected ExampleCriteriaMapper exampleCriteriaMapper; + protected ExampleCriteriaMapper exampleCriteriaMapper; public QueryByExampleElasticsearchExecutor(ElasticsearchOperations operations) { this.operations = operations; - this.exampleCriteriaMapper = new ExampleCriteriaMapper<>( - (MappingContext, ElasticsearchPersistentProperty>) operations - .getElasticsearchConverter().getMappingContext()); + this.exampleCriteriaMapper = new ExampleCriteriaMapper(operations.getElasticsearchConverter().getMappingContext()); } @Override public Optional findOne(Example example) { - CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withMaxResults(2).build(); SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), operations.getIndexCoordinatesFor(example.getProbeType())); if (searchHits.getTotalHits() > 1) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java index 3bdbdf0fe..37eade7b3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java @@ -15,36 +15,33 @@ */ package org.springframework.data.elasticsearch.repository.support.querybyexample; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + import org.reactivestreams.Publisher; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; -import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.function.Function; /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ public class ReactiveQueryByExampleElasticsearchExecutor implements ReactiveQueryByExampleExecutor { protected ReactiveElasticsearchOperations operations; - protected ExampleCriteriaMapper exampleCriteriaMapper; + protected ExampleCriteriaMapper exampleCriteriaMapper; public ReactiveQueryByExampleElasticsearchExecutor(ReactiveElasticsearchOperations operations) { this.operations = operations; - this.exampleCriteriaMapper = new ExampleCriteriaMapper<>( - (MappingContext, ElasticsearchPersistentProperty>) operations - .getElasticsearchConverter().getMappingContext()); + this.exampleCriteriaMapper = new ExampleCriteriaMapper(operations.getElasticsearchConverter().getMappingContext()); } @Override diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java index e0c7f80d9..7307fdb09 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java @@ -467,7 +467,7 @@ void shouldBuildRegexpQuery() throws JSONException { { "regexp": { "field1": { - "value": "\\\\[\\\\^abc\\\\]" + "value": "[^abc]" } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java index b1c910a79..114387cf0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java @@ -458,7 +458,7 @@ void shouldBuildRegexpQuery() throws JSONException { { "regexp": { "field1": { - "value": "\\\\[\\\\^abc\\\\]" + "value": "[^abc]" } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java index 87af236af..e9358052d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -25,6 +25,7 @@ /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) public class QueryByExampleElasticsearchExecutorELCIntegrationTests @@ -39,6 +40,5 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("query-by-example-repository"); } - } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java index f182c64a1..4001d1347 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -25,6 +25,7 @@ /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) public class QueryByExampleElasticsearchExecutorERHLCIntegrationTests @@ -39,6 +40,5 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("query-by-example-repository-es7"); } - } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java index 1ab145a53..c6fb55373 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java @@ -17,6 +17,8 @@ import org.assertj.core.api.AbstractThrowableAssert; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -37,6 +39,7 @@ import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.lang.Nullable; +import java.util.List; import java.util.Objects; import java.util.Optional; @@ -46,6 +49,7 @@ /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @SpringIntegrationTest abstract class QueryByExampleElasticsearchExecutorIntegrationTests { @@ -66,253 +70,415 @@ void cleanup() { operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); } - @Test // #2418 - void shouldFindOne() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - // when - SampleEntity probe = new SampleEntity(); - sampleEntity.setId(documentId); - Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); - - // then - assertThat(entityFromElasticSearch).contains(sampleEntity); + @Nested + @DisplayName("All QueryByExampleExecutor operations should work") + class QueryByExampleExecutorOperations { + @Test // #2418 + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity2); - } + } - @Test // #2418 - void shouldThrowExceptionIfMoreThanOneResultInFindOne() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("some message"); - sampleEntity2.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity2); - - // when - SampleEntity probe = new SampleEntity(); - AbstractThrowableAssert assertThatThrownBy = assertThatThrownBy( - () -> repository.findOne(Example.of(probe))); - - // then - assertThatThrownBy.isInstanceOf(IncorrectResultSizeDataAccessException.class); + @Test // #2418 + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); - } + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); - @Test // #2418 - void shouldFindOneWithNestedField() { - // given - SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); - sampleNestedEntity.setNestedData("sampleNestedData"); - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setSampleNestedEntity(sampleNestedEntity); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - // when - SampleEntity probe = new SampleEntity(); - sampleEntity.setSampleNestedEntity(sampleNestedEntity); - Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); - - // then - assertThat(entityFromElasticSearch).contains(sampleEntity); + repository.saveAll(List.of(sampleEntity, sampleEntity2)); - } + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("some message"); + final Example example = Example.of(probe); + AbstractThrowableAssert assertThatThrownBy = assertThatThrownBy( + () -> repository.findOne(example)); - @Test // #2418 - void shouldFindAll() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity2); - - // when - SampleEntity probe = new SampleEntity(); - probe.setMessage("hello world."); - Iterable sampleEntities = repository.findAll(Example.of(probe)); - - // then - assertThat(sampleEntities).isNotNull().hasSize(2); - } + // then + assertThatThrownBy.isInstanceOf(IncorrectResultSizeDataAccessException.class); - @Test // #2418 - void shouldFindAllWithMatchers() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity2); - - String documentId3 = nextIdAsString(); - SampleEntity sampleEntity3 = new SampleEntity(); - sampleEntity3.setId(documentId3); - sampleEntity3.setMessage("hola mundo."); - sampleEntity3.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity3); - - // when - SampleEntity probe = new SampleEntity(); - probe.setMessage("world"); - Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() - .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)))); - - // then - assertThat(sampleEntities).isNotNull().hasSize(2); - } + } - @Test // #2418 - void shouldFindAllWithSort() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setRate(1); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setRate(3); - sampleEntity2.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity2); - - String documentId3 = nextIdAsString(); - SampleEntity sampleEntity3 = new SampleEntity(); - sampleEntity3.setId(documentId3); - sampleEntity3.setMessage("hello world."); - sampleEntity3.setRate(2); - sampleEntity3.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity3); - - // when - SampleEntity probe = new SampleEntity(); - final Iterable all = repository.findAll(); - Iterable sampleEntities = repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")); - - // then - assertThat(sampleEntities).isNotNull().hasSize(3).containsExactly(sampleEntity2, sampleEntity3, sampleEntity); - } + @Test // #2418 + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + sampleNestedEntity.setAnotherNestedData("sampleAnotherNestedData"); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity.SampleNestedEntity sampleNestedEntity2 = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity2.setNestedData("sampleNestedData2"); + sampleNestedEntity2.setAnotherNestedData("sampleAnotherNestedData2"); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setSampleNestedEntity(sampleNestedEntity2); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity.SampleNestedEntity sampleNestedEntityProbe = new SampleEntity.SampleNestedEntity(); + sampleNestedEntityProbe.setNestedData("sampleNestedData"); + SampleEntity probe = new SampleEntity(); + probe.setSampleNestedEntity(sampleNestedEntityProbe); + + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity); - @Test // #2418 - void shouldFindAllWithPageable() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setRate(1); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setRate(3); - sampleEntity2.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity2); - - String documentId3 = nextIdAsString(); - SampleEntity sampleEntity3 = new SampleEntity(); - sampleEntity3.setId(documentId3); - sampleEntity3.setMessage("hello world."); - sampleEntity3.setRate(2); - sampleEntity3.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity3); - - // when - SampleEntity probe = new SampleEntity(); - Iterable page1 = repository.findAll(Example.of(probe), - PageRequest.of(0, 2, Sort.Direction.DESC, "rate")); - Iterable page2 = repository.findAll(Example.of(probe), - PageRequest.of(1, 2, Sort.Direction.DESC, "rate")); - - // then - assertThat(page1).isNotNull().hasSize(2).containsExactly(sampleEntity2, sampleEntity3); - assertThat(page2).isNotNull().hasSize(1).containsExactly(sampleEntity); - } + } + + @Test // #2418 + void shouldFindAll() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("bye world"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + Iterable sampleEntities = repository.findAll(Example.of(probe)); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void shouldFindAllWithSort() { + // given + SampleEntity sampleEntityWithRate11 = new SampleEntity(); + sampleEntityWithRate11.setDocumentId(nextIdAsString()); + sampleEntityWithRate11.setMessage("hello world"); + sampleEntityWithRate11.setRate(11); + sampleEntityWithRate11.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate13 = new SampleEntity(); + sampleEntityWithRate13.setDocumentId(nextIdAsString()); + sampleEntityWithRate13.setMessage("hello world"); + sampleEntityWithRate13.setRate(13); + sampleEntityWithRate13.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate22 = new SampleEntity(); + sampleEntityWithRate22.setDocumentId(nextIdAsString()); + sampleEntityWithRate22.setMessage("hello world"); + sampleEntityWithRate22.setRate(22); + sampleEntityWithRate22.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntityWithRate11, sampleEntityWithRate13, sampleEntityWithRate22)); + + // when + SampleEntity probe = new SampleEntity(); + final Iterable all = repository.findAll(); + Iterable sampleEntities = repository.findAll(Example.of(probe), + Sort.by(Sort.Direction.DESC, "rate")); + + // then + assertThat(sampleEntities).isNotNull().hasSize(3).containsExactly(sampleEntityWithRate22, sampleEntityWithRate13, + sampleEntityWithRate11); + } + + @Test // #2418 + void shouldFindAllWithPageable() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setRate(1); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setRate(3); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hello world"); + sampleEntity3.setRate(2); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + Iterable page1 = repository.findAll(Example.of(probe), + PageRequest.of(0, 2, Sort.Direction.DESC, "rate")); + Iterable page2 = repository.findAll(Example.of(probe), + PageRequest.of(1, 2, Sort.Direction.DESC, "rate")); + + // then + assertThat(page1).isNotNull().hasSize(2).containsExactly(sampleEntity2, sampleEntity3); + assertThat(page2).isNotNull().hasSize(1).containsExactly(sampleEntity); + } + + @Test // #2418 + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + final long count = repository.count(Example.of(probe)); + + // then + assertThat(count).isPositive(); + } + + @Test // #2418 + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + boolean exists = repository.exists(Example.of(probe)); + + // then + assertThat(exists).isTrue(); + } - @Test // #2418 - void shouldCount() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - // when - Long count = repository.count(Example.of(sampleEntity)); - - // then - assertThat(count).isPositive(); } - @Test // #2418 - void shouldExists() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity); - - // when - boolean exists = repository.exists(Example.of(sampleEntity)); - - // then - assertThat(exists).isTrue(); + @Nested + @DisplayName("All ExampleMatchers should work") + class AllExampleMatchersShouldWork { + + @Test // #2418 + void defaultStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.DEFAULT)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(1); + } + + @Test // #2418 + void exactStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("bye world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.EXACT)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(1); + } + + @Test // #2418 + void startingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("h"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void endingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void regexStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("[(hello)(hola)].*"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.REGEX)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + } @Document(indexName = "#{@indexNameProvider.indexName()}") static class SampleEntity { @Nullable - @Id private String id; + @Id private String documentId; @Nullable @Field(type = FieldType.Text, store = true, fielddata = true) private String type; @Nullable - @Field(type = FieldType.Text, store = true, fielddata = true) private String message; + @Field(type = FieldType.Keyword, store = true) private String message; @Nullable private Integer rate; @Nullable private Boolean available; @Nullable @@ -321,12 +487,12 @@ static class SampleEntity { @Version private Long version; @Nullable - public String getId() { - return id; + public String getDocumentId() { + return documentId; } - public void setId(@Nullable String id) { - this.id = id; + public void setDocumentId(@Nullable String documentId) { + this.documentId = documentId; } @Nullable @@ -392,11 +558,11 @@ public boolean equals(Object o) { SampleEntity that = (SampleEntity) o; - if (rate != that.rate) + if (!Objects.equals(rate, that.rate)) return false; if (available != that.available) return false; - if (!Objects.equals(id, that.id)) + if (!Objects.equals(documentId, that.documentId)) return false; if (!Objects.equals(type, that.type)) return false; @@ -409,11 +575,11 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = documentId != null ? documentId.hashCode() : 0; result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + (message != null ? message.hashCode() : 0); - result = 31 * result + rate; - result = 31 * result + (available ? 1 : 0); + result = 31 * result + (rate != null ? rate.hashCode() : 0); + result = 31 * result + (available != null ? available.hashCode() : 0); result = 31 * result + (sampleNestedEntity != null ? sampleNestedEntity.hashCode() : 0); result = 31 * result + (version != null ? version.hashCode() : 0); return result; @@ -424,6 +590,9 @@ static class SampleNestedEntity { @Nullable @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String anotherNestedData; + @Nullable public String getNestedData() { return nestedData; @@ -433,6 +602,15 @@ public void setNestedData(@Nullable String nestedData) { this.nestedData = nestedData; } + @Nullable + public String getAnotherNestedData() { + return anotherNestedData; + } + + public void setAnotherNestedData(@Nullable String anotherNestedData) { + this.anotherNestedData = anotherNestedData; + } + @Override public boolean equals(Object o) { if (this == o) @@ -442,12 +620,14 @@ public boolean equals(Object o) { SampleNestedEntity that = (SampleNestedEntity) o; - return Objects.equals(nestedData, that.nestedData); + return Objects.equals(nestedData, that.nestedData) && Objects.equals(anotherNestedData, that.anotherNestedData); } @Override public int hashCode() { - return nestedData != null ? nestedData.hashCode() : 0; + int result = nestedData != null ? nestedData.hashCode() : 0; + result = 31 * result + (anotherNestedData != null ? anotherNestedData.hashCode() : 0); + return result; } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java index 5063397e1..68227cca4 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -18,20 +18,21 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.test.context.ContextConfiguration; /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) public class ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests extends ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { @Configuration - @Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) static class Config { @Bean diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java index a25a38121..96b904a63 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -25,6 +25,7 @@ /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) public class ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java index 62bbf8c72..ff3f33ce9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.data.elasticsearch.repository.support.querybyexample; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -36,7 +38,7 @@ import org.springframework.lang.Nullable; import reactor.test.StepVerifier; -import java.util.Arrays; +import java.util.List; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +46,7 @@ /** * @author Ezequiel Antúnez Camacho + * @since 5.1 */ @SpringIntegrationTest abstract class ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { @@ -55,7 +58,10 @@ abstract class ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { @BeforeEach void before() { indexNameProvider.increment(); - operations.indexOps(SampleEntity.class).createWithMapping(); + operations.indexOps(SampleEntity.class).createWithMapping() + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); } @Test // #2418 @@ -64,224 +70,443 @@ void cleanup() { operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); } - @Test - void shouldFindOne() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - SampleEntity probe = new SampleEntity(); - sampleEntity.setId(documentId); - repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - // when - repository.findOne(Example.of(probe)) - // then - .as(StepVerifier::create) - .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity)) // - .verifyComplete(); - } + @Nested + @DisplayName("All QueryByExampleExecutor operations should work") + class ReactiveQueryByExampleExecutorOperations { + @Test + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity2)) // + .verifyComplete(); + } - @Test - void shouldThrowExceptionIfMoreThanOneResultInFindOne() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("some message"); - sampleEntity2.setVersion(System.currentTimeMillis()); - - repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2)).as(StepVerifier::create).expectNextCount(2L) - .verifyComplete(); + @Test + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectError(IncorrectResultSizeDataAccessException.class) // + .verify(); + } - // when - SampleEntity probe = new SampleEntity(); - repository.findOne(Example.of(probe)) - // then - .as(StepVerifier::create).expectError(IncorrectResultSizeDataAccessException.class).verify(); - } + @Test + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + sampleNestedEntity.setAnotherNestedData("sampleAnotherNestedData"); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity.SampleNestedEntity sampleNestedEntity2 = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity2.setNestedData("sampleNestedData2"); + sampleNestedEntity2.setAnotherNestedData("sampleAnotherNestedData2"); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setSampleNestedEntity(sampleNestedEntity2); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity.SampleNestedEntity sampleNestedEntityProbe = new SampleEntity.SampleNestedEntity(); + sampleNestedEntityProbe.setNestedData("sampleNestedData"); + SampleEntity probe = new SampleEntity(); + probe.setSampleNestedEntity(sampleNestedEntityProbe); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity) // + .verifyComplete(); + } - @Test - void shouldFindOneWithNestedField() { - // given - SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); - sampleNestedEntity.setNestedData("sampleNestedData"); - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setSampleNestedEntity(sampleNestedEntity); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - // when - SampleEntity probe = new SampleEntity(); - sampleEntity.setSampleNestedEntity(sampleNestedEntity); - repository.findOne(Example.of(probe)) - // then - .as(StepVerifier::create) - .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity)) // - .verifyComplete(); - } + @Test + void shouldFindAll() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("bye world"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + repository.findAll(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity2)) // + .verifyComplete(); + } - @Test - void shouldFindAll() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setVersion(System.currentTimeMillis()); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setVersion(System.currentTimeMillis()); - - repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2)).as(StepVerifier::create).expectNextCount(2L) - .verifyComplete(); + @Test + void shouldFindAllWithSort() { + // given + SampleEntity sampleEntityWithRate11 = new SampleEntity(); + sampleEntityWithRate11.setDocumentId(nextIdAsString()); + sampleEntityWithRate11.setMessage("hello world"); + sampleEntityWithRate11.setRate(11); + sampleEntityWithRate11.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate13 = new SampleEntity(); + sampleEntityWithRate13.setDocumentId(nextIdAsString()); + sampleEntityWithRate13.setMessage("hello world"); + sampleEntityWithRate13.setRate(13); + sampleEntityWithRate13.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate22 = new SampleEntity(); + sampleEntityWithRate22.setDocumentId(nextIdAsString()); + sampleEntityWithRate22.setMessage("hello world"); + sampleEntityWithRate22.setRate(22); + sampleEntityWithRate22.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntityWithRate11, sampleEntityWithRate13, sampleEntityWithRate22)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntityWithRate22, sampleEntityWithRate13, sampleEntityWithRate11)) // + .verifyComplete(); + } - // when - SampleEntity probe = new SampleEntity(); - probe.setMessage("hello world."); - repository.findAll(Example.of(probe)) - // then - .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity, sampleEntity2)).verifyComplete(); - } + @Test + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.count(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } - @Test - void shouldFindAllWithMatchers() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setVersion(System.currentTimeMillis()); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("bye world."); - sampleEntity2.setVersion(System.currentTimeMillis()); - - String documentId3 = nextIdAsString(); - SampleEntity sampleEntity3 = new SampleEntity(); - sampleEntity3.setId(documentId3); - sampleEntity3.setMessage("hola mundo."); - sampleEntity3.setVersion(System.currentTimeMillis()); - - repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2, sampleEntity3)).as(StepVerifier::create) - .expectNextCount(3L).verifyComplete(); - - // when - SampleEntity probe = new SampleEntity(); - probe.setMessage("world"); - repository - .findAll(Example.of(probe, - ExampleMatcher.matching().withMatcher("message", - ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)))) - // then - .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity, sampleEntity2)).verifyComplete(); - } + @Test + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.exists(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + } - @Test - void shouldFindAllWithSort() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("hello world."); - sampleEntity.setRate(1); - sampleEntity.setVersion(System.currentTimeMillis()); - - String documentId2 = nextIdAsString(); - SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("hello world."); - sampleEntity2.setRate(3); - sampleEntity2.setVersion(System.currentTimeMillis()); - - String documentId3 = nextIdAsString(); - SampleEntity sampleEntity3 = new SampleEntity(); - sampleEntity3.setId(documentId3); - sampleEntity3.setMessage("hello world."); - sampleEntity3.setRate(2); - sampleEntity3.setVersion(System.currentTimeMillis()); - - repository.saveAll(Arrays.asList(sampleEntity, sampleEntity2, sampleEntity3)).as(StepVerifier::create) - .expectNextCount(3L).verifyComplete(); - - // when - SampleEntity probe = new SampleEntity(); - repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")) - // then - .as(StepVerifier::create).expectNextSequence(Arrays.asList(sampleEntity2, sampleEntity3, sampleEntity)) - .verifyComplete(); - ; } - @Test - void shouldCount() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - // when - repository.count(Example.of(sampleEntity)) - // then - .as(StepVerifier::create).expectNext(1L).verifyComplete(); - } + @Nested + @DisplayName("All ExampleMatchers should work") + class AllExampleMatchersShouldWork { + + @Test // #2418 + void defaultStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.DEFAULT)))) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity) // + .verifyComplete(); + } + + @Test // #2418 + void exactStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("bye world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.EXACT)))) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity2) // + .verifyComplete(); + } + + @Test // #2418 + void startingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("h"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity3)) // + .verifyComplete(); + } + + @Test // #2418 + void endingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity2)) // + .verifyComplete(); + } + + @Test // #2418 + void regexStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("[(hello)(hola)].*"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.REGEX)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity3)) // + .verifyComplete(); + } - @Test - void shouldExists() { - // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - repository.save(sampleEntity).as(StepVerifier::create).expectNextCount(1L).verifyComplete(); - - // when - repository.exists(Example.of(sampleEntity)) - // then - .as(StepVerifier::create).expectNext(true).verifyComplete(); } @Document(indexName = "#{@indexNameProvider.indexName()}") static class SampleEntity { @Nullable - @Id private String id; + @Id private String documentId; @Nullable @Field(type = FieldType.Text, store = true, fielddata = true) private String type; @Nullable - @Field(type = FieldType.Text, store = true, fielddata = true) private String message; + @Field(type = FieldType.Keyword, store = true) private String message; @Nullable private Integer rate; @Nullable private Boolean available; @Nullable - @Field(type = FieldType.Nested, store = true, fielddata = true) private SampleNestedEntity sampleNestedEntity; + @Field(type = FieldType.Nested, store = true, + fielddata = true) private SampleEntity.SampleNestedEntity sampleNestedEntity; @Nullable @Version private Long version; @Nullable - public String getId() { - return id; + public String getDocumentId() { + return documentId; } - public void setId(@Nullable String id) { - this.id = id; + public void setDocumentId(@Nullable String documentId) { + this.documentId = documentId; } @Nullable @@ -321,11 +546,11 @@ public void setAvailable(Boolean available) { } @Nullable - public SampleNestedEntity getSampleNestedEntity() { + public SampleEntity.SampleNestedEntity getSampleNestedEntity() { return sampleNestedEntity; } - public void setSampleNestedEntity(SampleNestedEntity sampleNestedEntity) { + public void setSampleNestedEntity(SampleEntity.SampleNestedEntity sampleNestedEntity) { this.sampleNestedEntity = sampleNestedEntity; } @@ -347,11 +572,11 @@ public boolean equals(Object o) { SampleEntity that = (SampleEntity) o; - if (rate != that.rate) + if (!Objects.equals(rate, that.rate)) return false; if (available != that.available) return false; - if (!Objects.equals(id, that.id)) + if (!Objects.equals(documentId, that.documentId)) return false; if (!Objects.equals(type, that.type)) return false; @@ -364,7 +589,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = id != null ? id.hashCode() : 0; + int result = documentId != null ? documentId.hashCode() : 0; result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + (message != null ? message.hashCode() : 0); result = 31 * result + (rate != null ? rate.hashCode() : 0); @@ -379,6 +604,9 @@ static class SampleNestedEntity { @Nullable @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String anotherNestedData; + @Nullable public String getNestedData() { return nestedData; @@ -388,6 +616,15 @@ public void setNestedData(@Nullable String nestedData) { this.nestedData = nestedData; } + @Nullable + public String getAnotherNestedData() { + return anotherNestedData; + } + + public void setAnotherNestedData(@Nullable String anotherNestedData) { + this.anotherNestedData = anotherNestedData; + } + @Override public boolean equals(Object o) { if (this == o) @@ -395,14 +632,16 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - SampleNestedEntity that = (SampleNestedEntity) o; + SampleEntity.SampleNestedEntity that = (SampleEntity.SampleNestedEntity) o; - return Objects.equals(nestedData, that.nestedData); + return Objects.equals(nestedData, that.nestedData) && Objects.equals(anotherNestedData, that.anotherNestedData); } @Override public int hashCode() { - return nestedData != null ? nestedData.hashCode() : 0; + int result = nestedData != null ? nestedData.hashCode() : 0; + result = 31 * result + (anotherNestedData != null ? anotherNestedData.hashCode() : 0); + return result; } } }