Fix handling of array-of-strings parameters for @Query-annotated queries.

Original Pull Request  #2182
Closes #2135

(cherry picked from commit 259c43af19db269476d31bfe5df5554807b7b2f9)
(cherry picked from commit aa4aecacdf0596578577461af5129cf12c9a5c7b)
This commit is contained in:
diamondT 2022-06-27 21:35:06 +03:00 committed by Peter-Josef Meisch
parent c58e3b3a8f
commit 474ff84d6e
4 changed files with 227 additions and 169 deletions

View File

@ -82,8 +82,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
Query query = createQuery(
new ConvertingParameterAccessor(elasticsearchOperations.getElasticsearchConverter(), parameterAccessor));
Query query = createQuery(parameterAccessor);
if (queryMethod.hasAnnotatedHighlight()) {
query.setHighlightQuery(queryMethod.getAnnotatedHighlightQuery());

View File

@ -1,95 +0,0 @@
/*
* Copyright 2019-2021 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.query;
import java.util.Iterator;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @since 3.2
*/
public class ConvertingParameterAccessor implements ElasticsearchParameterAccessor {
private final ElasticsearchConverter converter;
private final ElasticsearchParameterAccessor delegate;
public ConvertingParameterAccessor(ElasticsearchConverter converter, ElasticsearchParameterAccessor delegate) {
this.converter = converter;
this.delegate = delegate;
}
@Override
public Object[] getValues() {
return delegate.getValues();
}
@Override
public Pageable getPageable() {
return delegate.getPageable();
}
@Override
public Sort getSort() {
return delegate.getSort();
}
@Override
public Optional<Class<?>> getDynamicProjection() {
return delegate.getDynamicProjection();
}
@Override
public Class<?> findDynamicProjection() {
return delegate.findDynamicProjection();
}
@Override
public Object getBindableValue(int index) {
return getConvertedValue(delegate.getBindableValue(index));
}
@Override
public boolean hasBindableNullValue() {
return delegate.hasBindableNullValue();
}
@Override
public Iterator<Object> iterator() {
return delegate.iterator();
}
@Nullable
private Object getConvertedValue(Object value) {
if (value == null) {
return "null";
}
if (converter.getConversionService().canConvert(value.getClass(), String.class)) {
return converter.getConversionService().convert(value, String.class);
}
return value.toString();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2021 the original author or authors.
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -144,10 +144,37 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
.isEqualTo("{ 'bool' : { 'must' : { 'term' : { 'car' : 'Toyota-Prius' } } } }");
}
@Test // #2135
@DisplayName("should handle array-of-strings parameters correctly")
void shouldHandleArrayOfStringsParametersCorrectly() throws Exception {
List<String> otherNames = Arrays.asList("Wesley", "Emmett");
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByOtherNames", otherNames);
assertThat(query).isInstanceOf(StringQuery.class);
assertThat(((StringQuery) query).getSource())
.isEqualTo("{ 'bool' : { 'must' : { 'terms' : { 'otherNames' : [\"Wesley\",\"Emmett\"] } } } }");
}
@Test // #2135
@DisplayName("should handle array-of-Integers parameters correctly")
void shouldHandleArrayOfIntegerParametersCorrectly() throws Exception {
List<Integer> ages = Arrays.asList(42, 57);
org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAges", ages);
assertThat(query).isInstanceOf(StringQuery.class);
assertThat(((StringQuery) query).getSource())
.isEqualTo("{ 'bool' : { 'must' : { 'terms' : { 'ages' : [42,57] } } } }");
}
private org.springframework.data.elasticsearch.core.query.Query createQuery(String methodName, Object... args)
throws NoSuchMethodException {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass)
.map(clazz -> Collection.class.isAssignableFrom(clazz) ? List.class : clazz).toArray(Class[]::new);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes);
ReactiveElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
@ -195,6 +222,12 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
@Query("{ 'bool' : { 'must' : { 'term' : { 'car' : '?0' } } } }")
Mono<Person> findByCar(Car car);
@Query("{ 'bool' : { 'must' : { 'terms' : { 'otherNames' : ?0 } } } }")
Flux<Person> findByOtherNames(List<String> otherNames);
@Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }")
Flux<Person> findByAges(List<Integer> ages);
}
/**
@ -206,10 +239,13 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
@Document(indexName = "test-index-person-reactive-repository-string-query")
public class Person {
@Nullable @Id private String id;
@Nullable
@Id private String id;
@Nullable private String name;
@Nullable private List<String> otherNames;
@Nullable @Field(type = FieldType.Nested) private List<Car> car;
@Nullable @Field(type = FieldType.Nested, includeInParent = true) private List<Book> books;
@ -232,6 +268,15 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
this.name = name;
}
@Nullable
public List<String> getOtherNames() {
return otherNames;
}
public void setOtherNames(List<String> otherNames) {
this.otherNames = otherNames;
}
@Nullable
public List<Car> getCar() {
return car;

View File

@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import static org.springframework.data.elasticsearch.core.query.Query.*;
import org.springframework.data.elasticsearch.annotations.FieldType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -91,7 +92,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
operations.indexOps(IndexCoordinates.of(INDEX)).delete().block();
}
@Test // DATAES-519
@Test
// DATAES-519
void saveShouldSaveSingleEntity() {
repository.save(new SampleEntity()) //
@ -105,7 +107,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
return operations.exists(id, IndexCoordinates.of(INDEX));
}
@Test // DATAES-519
@Test
// DATAES-519
void saveShouldComputeMultipleEntities() {
repository.saveAll(Arrays.asList(new SampleEntity(), new SampleEntity(), new SampleEntity()))
@ -118,14 +121,16 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519, DATAES-767, DATAES-822
@Test
// DATAES-519, DATAES-767, DATAES-822
void findByIdShouldErrorIfIndexDoesNotExist() {
repository.findById("id-two") //
.as(StepVerifier::create) //
.expectError(RestStatusException.class);
}
@Test // DATAES-519
@Test
// DATAES-519
void findShouldRetrieveSingleEntityById() {
bulkIndex(new SampleEntity("id-one"), //
@ -138,7 +143,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void findByIdShouldCompleteIfNothingFound() {
bulkIndex(new SampleEntity("id-one"), //
@ -150,7 +156,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-720
@Test
// DATAES-720
void findAllShouldReturnAllElements() {
// make sure to be above the default page size of the Query interface
int count = DEFAULT_PAGE_SIZE * 2;
@ -165,12 +172,14 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void findAllByIdByIdShouldCompleteIfIndexDoesNotExist() {
repository.findAllById(Arrays.asList("id-two", "id-two")).as(StepVerifier::create).verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void findAllByIdShouldRetrieveMatchingDocuments() {
bulkIndex(new SampleEntity("id-one"), //
@ -185,7 +194,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void findAllByIdShouldCompleteWhenNothingFound() {
bulkIndex(new SampleEntity("id-one"), //
@ -198,7 +208,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-717
@Test
// DATAES-717
void shouldReturnFluxOfSearchHit() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -213,7 +224,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-717
@Test
// DATAES-717
void shouldReturnFluxOfSearchHitForStringQuery() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -228,7 +240,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-372
@Test
// DATAES-372
void shouldReturnHighlightsOnAnnotatedMethod() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -246,7 +259,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-372
@Test
// DATAES-372
void shouldReturnHighlightsOnAnnotatedStringQueryMethod() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -264,14 +278,16 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519, DATAES-767, DATAES-822
@Test
// DATAES-519, DATAES-767, DATAES-822
void countShouldErrorWhenIndexDoesNotExist() {
repository.count() //
.as(StepVerifier::create) //
.expectError(RestStatusException.class);
}
@Test // DATAES-519
@Test
// DATAES-519
void countShouldCountDocuments() {
bulkIndex(new SampleEntity("id-one"), //
@ -281,7 +297,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
repository.count().as(StepVerifier::create).expectNext(2L).verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void existsByIdShouldReturnTrueIfExists() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -295,7 +312,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void existsByIdShouldReturnFalseIfNotExists() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -309,7 +327,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void countShouldCountMatchingDocuments() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -336,7 +355,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void existsShouldReturnTrueIfExists() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -350,7 +370,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void existsShouldReturnFalseIfNotExists() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -364,7 +385,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void deleteByIdShouldCompleteIfNothingDeleted() {
bulkIndex(new SampleEntity("id-one"), //
@ -374,14 +396,16 @@ class SimpleReactiveElasticsearchRepositoryTests {
repository.deleteById("does-not-exist").as(StepVerifier::create).verifyComplete();
}
@Test // DATAES-519, DATAES-767, DATAES-822, DATAES-678
@Test
// DATAES-519, DATAES-767, DATAES-822, DATAES-678
void deleteByIdShouldCompleteWhenIndexDoesNotExist() {
repository.deleteById("does-not-exist") //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void deleteByIdShouldDeleteEntry() {
SampleEntity toBeDeleted = new SampleEntity("id-two");
@ -393,19 +417,22 @@ class SimpleReactiveElasticsearchRepositoryTests {
assertThat(documentWithIdExistsInIndex(toBeDeleted.getId()).block()).isFalse();
}
@Test // DATAES-976
@Test
// DATAES-976
void deleteAllByIdShouldDeleteEntry() {
SampleEntity toBeDeleted = new SampleEntity("id-two");
bulkIndex(new SampleEntity("id-one"), toBeDeleted) //
.block();
repository.deleteAllById(Collections.singletonList(toBeDeleted.getId())).as(StepVerifier::create).verifyComplete();
repository.deleteAllById(Collections.singletonList(toBeDeleted.getId())).as(StepVerifier::create)
.verifyComplete();
assertThat(documentWithIdExistsInIndex(toBeDeleted.getId()).block()).isFalse();
}
@Test // DATAES-519
@Test
// DATAES-519
void deleteShouldDeleteEntry() {
SampleEntity toBeDeleted = new SampleEntity("id-two");
@ -417,7 +444,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
assertThat(documentWithIdExistsInIndex(toBeDeleted.getId()).block()).isFalse();
}
@Test // DATAES-519
@Test
// DATAES-519
void deleteAllShouldDeleteGivenEntries() {
SampleEntity toBeDeleted = new SampleEntity("id-one");
@ -434,7 +462,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
assertThat(documentWithIdExistsInIndex(hangInThere.getId()).block()).isTrue();
}
@Test // DATAES-519
@Test
// DATAES-519
void deleteAllShouldDeleteAllEntries() {
bulkIndex(new SampleEntity("id-one"), //
@ -450,7 +479,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderMethodShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -464,7 +494,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderMethodShouldBeExecutedCorrectlyWhenGivenPublisher() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -478,7 +509,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderWithDerivedSortMethodShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "test", 3), //
@ -494,7 +526,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderMethodWithSortParameterShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "test", 3), //
@ -510,7 +543,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderMethodWithPageableParameterShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "test", 3), //
@ -525,7 +559,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedFinderMethodReturningMonoShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -539,7 +574,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void annotatedFinderMethodShouldBeExecutedCorrectly() {
int count = 30;
@ -555,7 +591,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // #1917
@Test
// #1917
void annotatedFinderMethodPagedShouldBeExecutedCorrectly() {
int count = 30;
@ -575,7 +612,8 @@ class SimpleReactiveElasticsearchRepositoryTests {
.verifyComplete();
}
@Test // DATAES-519
@Test
// DATAES-519
void derivedDeleteMethodShouldBeExecutedCorrectly() {
bulkIndex(new SampleEntity("id-one", "message"), //
@ -593,6 +631,56 @@ class SimpleReactiveElasticsearchRepositoryTests {
assertThat(documentWithIdExistsInIndex("id-three").block()).isTrue();
}
@Test
// #2135
void FluxOfSearchHitForArrayQuery() {
bulkIndex(new SampleEntity("id-one", "message1"), //
new SampleEntity("id-two", "message2"), //
new SampleEntity("id-three", "message3")) //
.block();
repository.findAllViaAnnotatedQueryByMessageIn(Arrays.asList("message1", "message3")) //
.as(StepVerifier::create) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-one")) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
.verifyComplete();
}
@Test
// #2135
void FluxOfSearchHitForIntegerArrayQuery() {
bulkIndex(new SampleEntity("id-one", "test", 3), //
new SampleEntity("id-two", "test test", 1), //
new SampleEntity("id-three", "test test", 2)) //
.block();
repository.findAllViaAnnotatedQueryByRatesIn(Arrays.asList(2, 3)) //
.as(StepVerifier::create) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-one")) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
.verifyComplete();
}
@Test
// #2135
void FluxOfSearchHitForStringAndIntegerArrayQuery() {
bulkIndex(new SampleEntity("id-one", "message1", 1), //
new SampleEntity("id-two", "message2", 2), //
new SampleEntity("id-three", "message3", 3), //
new SampleEntity("id-four", "message4", 4), //
new SampleEntity("id-five", "message5", 5)) //
.block();
repository.findAllViaAnnotatedQueryByMessageInAndRatesIn(Arrays.asList("message5", "message3"), Arrays.asList(2,
3)) //
.as(StepVerifier::create) //
.consumeNextWith(it -> assertThat(it.getId()).isEqualTo("id-three")) //
.verifyComplete();
}
Mono<Void> bulkIndex(SampleEntity... entities) {
return operations.saveAll(Arrays.asList(entities), IndexCoordinates.of(INDEX)).then();
}
@ -609,11 +697,11 @@ class SimpleReactiveElasticsearchRepositoryTests {
Flux<SampleEntity> findAllByMessage(Publisher<String> message);
@Highlight(fields = { @HighlightField(name = "message") })
@Highlight(fields = {@HighlightField(name = "message")})
Flux<SearchHit<SampleEntity>> queryAllByMessage(String message);
@Query("{\"bool\": {\"must\": [{\"term\": {\"message\": \"?0\"}}]}}")
@Highlight(fields = { @HighlightField(name = "message") })
@Highlight(fields = {@HighlightField(name = "message")})
Flux<SearchHit<SampleEntity>> queryByMessageWithString(String message);
@Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }")
@ -632,18 +720,39 @@ class SimpleReactiveElasticsearchRepositoryTests {
@CountQuery(value = "{\"bool\": {\"must\": [{\"term\": {\"message\": \"?0\"}}]}}")
Mono<Long> retrieveCountByText(String message);
@Query("{ \"terms\": { \"message\": ?0 } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageIn(List<String> messages);
@Query("{ \"terms\": { \"rate\": ?0 } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByRatesIn(List<Integer> rates);
@Query("{\"bool\": {\"must\": [{ \"terms\": { \"message\": ?0 } }, { \"terms\": { \"rate\": ?1 } }] } }")
Flux<SampleEntity> findAllViaAnnotatedQueryByMessageInAndRatesIn(List<String> messages, List<Integer> rates);
}
@Document(indexName = INDEX)
static class SampleEntity {
@Nullable @Id private String id;
@Nullable @Field(type = Text, store = true, fielddata = true) private String type;
@Nullable @Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
@Nullable private boolean available;
@Nullable @Version private Long version;
@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 int rate;
@Nullable
private boolean available;
@Nullable
@Version
private Long version;
public SampleEntity() {}
public SampleEntity() {
}
public SampleEntity(@Nullable String id) {
this.id = id;