diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java index cec28377..1cfb0a5a 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java @@ -1,5 +1,6 @@ package com.redis.om.spring.repository.support; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -17,7 +18,8 @@ import com.redis.om.spring.repository.RedisDocumentRepository; import com.redis.om.spring.search.stream.EntityStream; import com.redis.om.spring.search.stream.EntityStreamImpl; -import com.redis.om.spring.search.stream.FluentQueryByExample; +import com.redis.om.spring.search.stream.RedisFluentQueryByExample; +import com.redis.om.spring.search.stream.SearchStream; import com.redis.om.spring.serialization.gson.GsonListOfType; import com.redis.om.spring.util.ObjectUtils; import com.redis.om.spring.vectorize.FeatureExtractor; @@ -26,6 +28,7 @@ import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Version; @@ -89,7 +92,9 @@ public SimpleRedisDocumentRepository( // KeyValueOperations operations, // @Qualifier("redisModulesOperations") RedisModulesOperations rmo, // RediSearchIndexer indexer, // - RedisMappingContext mappingContext, GsonBuilder gsonBuilder, FeatureExtractor featureExtractor, // + RedisMappingContext mappingContext, // + GsonBuilder gsonBuilder, // + FeatureExtractor featureExtractor, // RedisOMProperties properties) { super(metadata, operations); this.modulesOperations = (RedisModulesOperations) rmo; @@ -373,7 +378,13 @@ private Number getEntityVersion(String key, String versionProperty) { @Override public Optional findOne(Example example) { - return entityStream.of(example.getProbeType()).filter(example).findFirst(); + Iterable result = findAll(example); + var size = Iterables.size(result); + if (size > 1) { + throw new IncorrectResultSizeDataAccessException("Query returned non unique result", 1); + } + + return StreamSupport.stream(result.spliterator(), false).findFirst(); } @Override @@ -388,7 +399,13 @@ public Iterable findAll(Example example, Sort sort) { @Override public Page findAll(Example example, Pageable pageable) { - return pageFromSlice(entityStream.of(example.getProbeType()).filter(example).getSlice(pageable)); + SearchStream stream = entityStream.of(example.getProbeType()); + var offset = pageable.getOffset() * pageable.getPageSize(); + var limit = pageable.getPageSize(); + Slice slice = stream.filter(example).loadAll().limit(limit, Math.toIntExact(offset)) + .toList(pageable, stream.getEntityClass()); + + return pageFromSlice(slice); } /* (non-Javadoc) @@ -467,7 +484,8 @@ public R findBy(Example example, Function(example, example.getProbeType(), entityStream, getSearchOps())); + new RedisFluentQueryByExample<>(example, example.getProbeType(), entityStream, getSearchOps(), + mappingConverter.getMappingContext())); } private SearchOperations getSearchOps() { diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java index ef7aada2..7f90350d 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java @@ -1,5 +1,6 @@ package com.redis.om.spring.repository.support; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.redis.om.spring.RedisEnhancedKeyValueAdapter; import com.redis.om.spring.RedisOMProperties; @@ -14,10 +15,12 @@ import com.redis.om.spring.repository.RedisEnhancedRepository; import com.redis.om.spring.search.stream.EntityStream; import com.redis.om.spring.search.stream.EntityStreamImpl; -import com.redis.om.spring.search.stream.FluentQueryByExample; +import com.redis.om.spring.search.stream.RedisFluentQueryByExample; +import com.redis.om.spring.search.stream.SearchStream; import com.redis.om.spring.util.ObjectUtils; import com.redis.om.spring.vectorize.FeatureExtractor; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.*; import org.springframework.data.domain.Sort.Order; import org.springframework.data.keyvalue.core.IterableConverter; @@ -299,7 +302,13 @@ private boolean expires(RedisData data) { @Override public Optional findOne(Example example) { - return entityStream.of(example.getProbeType()).filter(example).findFirst(); + Iterable result = findAll(example); + var size = Iterables.size(result); + if (size > 1) { + throw new IncorrectResultSizeDataAccessException("Query returned non unique result", 1); + } + + return StreamSupport.stream(result.spliterator(), false).findFirst(); } @Override @@ -314,7 +323,13 @@ public Iterable findAll(Example example, Sort sort) { @Override public Page findAll(Example example, Pageable pageable) { - return pageFromSlice(entityStream.of(example.getProbeType()).filter(example).getSlice(pageable)); + SearchStream stream = entityStream.of(example.getProbeType()); + var offset = pageable.getOffset() * pageable.getPageSize(); + var limit = pageable.getPageSize(); + Slice slice = stream.filter(example).loadAll().limit(limit, Math.toIntExact(offset)) + .toList(pageable, stream.getEntityClass()); + + return pageFromSlice(slice); } @Override @@ -337,7 +352,8 @@ public R findBy(Example example, Function(example, example.getProbeType(), entityStream, getSearchOps())); + new RedisFluentQueryByExample<>(example, example.getProbeType(), entityStream, getSearchOps(), + mappingConverter.getMappingContext())); } private SearchOperations getSearchOps() { diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStream.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStream.java index 5a0b4ae1..6e37bf12 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStream.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStream.java @@ -3,7 +3,7 @@ import com.redis.om.spring.annotations.ReducerFunction; import com.redis.om.spring.metamodel.MetamodelField; import com.redis.om.spring.search.stream.aggregations.filters.AggregationFilter; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort.Order; import redis.clients.jedis.search.aggr.AggregationResult; @@ -53,7 +53,7 @@ public interface AggregationStream { // Cursor API AggregationStream cursor(int i, Duration duration); - Slice toList(PageRequest pageRequest, Class... contentTypes); + Slice toList(Pageable pageRequest, Class... contentTypes); - Slice toList(PageRequest pageRequest, Duration duration, Class... contentTypes); + Slice toList(Pageable pageRequest, Duration duration, Class... contentTypes); } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStreamImpl.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStreamImpl.java index 3abcc49b..4a55ca2b 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStreamImpl.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/AggregationStreamImpl.java @@ -10,7 +10,7 @@ import com.redis.om.spring.search.stream.aggregations.filters.AggregationFilter; import com.redis.om.spring.tuple.Tuples; import com.redis.om.spring.util.ObjectUtils; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; @@ -208,7 +208,7 @@ public AggregationStream limit(int limit) { } @Override - public AggregationStream limit(int offset, int limit) { + public AggregationStream limit(int limit, int offset) { applyCurrentGroupBy(); aggregation.limit(offset, limit); limitSet = true; @@ -387,7 +387,7 @@ public AggregationStream cursor(int count, Duration timeout) { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Slice toList(PageRequest pageRequest, Class... contentTypes) { + public Slice toList(Pageable pageRequest, Class... contentTypes) { applyCurrentGroupBy(); aggregation.cursor(pageRequest.getPageSize(), 300000); return new AggregationPage(this, pageRequest, entityClass, gson, mappingConverter, isDocument); @@ -395,7 +395,7 @@ public Slice toList(PageRequest pageRequest, Class... conten @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Slice toList(PageRequest pageRequest, Duration timeout, Class... contentTypes) { + public Slice toList(Pageable pageRequest, Duration timeout, Class... contentTypes) { applyCurrentGroupBy(); aggregation.cursor(pageRequest.getPageSize(), timeout.toMillis()); return new AggregationPage(this, pageRequest, entityClass, gson, mappingConverter, isDocument); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/FluentQueryByExample.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/FluentQueryByExample.java deleted file mode 100644 index f3e458a0..00000000 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/FluentQueryByExample.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.redis.om.spring.search.stream; - -import com.redis.om.spring.metamodel.MetamodelField; -import com.redis.om.spring.metamodel.MetamodelUtils; -import com.redis.om.spring.ops.search.SearchOperations; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.domain.*; -import org.springframework.data.repository.query.FluentQuery; -import redis.clients.jedis.search.Query; -import redis.clients.jedis.search.SearchResult; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class FluentQueryByExample implements FluentQuery.FetchableFluentQuery { - private final SearchStream searchStream; - private final Class probeType; - - private final SearchOperations searchOps; - - public FluentQueryByExample( // - Example example, // - Class probeType, // - EntityStream entityStream, // - SearchOperations searchOps // - ) { - this.probeType = probeType; - this.searchOps = searchOps; - this.searchStream = entityStream.of(probeType); - searchStream.filter(example); - } - - @Override - public FetchableFluentQuery sortBy(Sort sort) { - searchStream.sorted(sort); - return this; - } - - @Override - public FetchableFluentQuery as(Class resultType) { - throw new UnsupportedOperationException("`as` is not supported on a Redis Repositories"); - } - - @Override - @SuppressWarnings("unchecked") - public FetchableFluentQuery project(Collection properties) { - List> metamodelFields = MetamodelUtils.getMetamodelFieldsForProperties(probeType, properties); - metamodelFields.forEach(mmf -> searchStream.project((MetamodelField) mmf)); - return this; - } - - @Override - public T oneValue() { - var result = searchStream.collect(Collectors.toList()); - - if (org.springframework.util.ObjectUtils.isEmpty(result)) { - return null; - } - - if (result.size() > 1) { - throw new IncorrectResultSizeDataAccessException("Query returned non unique result", 1); - } - - return result.iterator().next(); - } - - @Override - public T firstValue() { - return searchStream.findFirst().orElse(null); - } - - @Override - public List all() { - return searchStream.collect(Collectors.toList()); - } - - @Override - public Page page(Pageable pageable) { - Query query = (searchStream.backingQuery().isBlank()) ? new Query() : new Query(searchStream.backingQuery()); - query.limit(0, 0); - SearchResult searchResult = searchOps.search(query); - var count = searchResult.getTotalResults(); - var pageContents = searchStream.limit(pageable.getPageSize()).skip(pageable.getOffset()) - .collect(Collectors.toList()); - return new PageImpl<>(pageContents, pageable, count); - } - - @Override - public Stream stream() { - return all().stream(); - } - - @Override - public long count() { - return searchStream.count(); - } - - @Override - public boolean exists() { - return searchStream.count() > 0; - } -} diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java new file mode 100644 index 00000000..e99c6cb8 --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java @@ -0,0 +1,190 @@ +package com.redis.om.spring.search.stream; + +import com.redis.om.spring.metamodel.MetamodelField; +import com.redis.om.spring.metamodel.MetamodelUtils; +import com.redis.om.spring.ops.search.SearchOperations; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.convert.DtoInstantiatingConverter; +import org.springframework.data.domain.*; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import redis.clients.jedis.search.Query; +import redis.clients.jedis.search.SearchResult; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RedisFluentQueryByExample implements FetchableFluentQuery { + + private final Example example; + private final Sort sort; + private final Class domainType; + private final Class resultType; + + private final SearchOperations searchOps; + private final SearchStream searchStream; + private SearchStream parentSearchStream; + private final EntityStream entityStream; + private boolean isProjection = false; + private final SpelAwareProxyProjectionFactory projectionFactory; + private final RedisMappingContext mappingContext; + private final EntityInstantiators entityInstantiators = new EntityInstantiators(); + private Function conversionFunction; + + public RedisFluentQueryByExample( // + Example example, // + Class resultType, // + EntityStream entityStream, // + SearchOperations searchOps, // + RedisMappingContext mappingContext + ) { + this(example, Sort.unsorted(), resultType, resultType, entityStream, searchOps, mappingContext); + } + + public RedisFluentQueryByExample( // + Example example, // + Sort sort, // + Class domainType, // + Class resultType, // + EntityStream entityStream, // + SearchOperations searchOps, // + SearchStream searchStream, // + RedisMappingContext mappingContext + ) { + this.example = example; + this.sort = sort; + this.domainType = domainType; + this.resultType = resultType; + this.entityStream = entityStream; + this.searchOps = searchOps; + this.searchStream = null; + this.parentSearchStream = searchStream; + this.isProjection = true; + this.projectionFactory = new SpelAwareProxyProjectionFactory(); + this.mappingContext = mappingContext; + this.conversionFunction = getConversionFunction(domainType, resultType); + } + + public RedisFluentQueryByExample( // + Example example, // + Sort sort, // + Class domainType, // + Class resultType, // + EntityStream entityStream, // + SearchOperations searchOps, // + RedisMappingContext mappingContext + ) { + this.example = example; + this.sort = sort; + this.domainType = domainType; + this.resultType = resultType; + this.entityStream = entityStream; + this.searchOps = searchOps; + this.searchStream = entityStream.of(resultType); + this.projectionFactory = new SpelAwareProxyProjectionFactory(); + this.mappingContext = mappingContext; + searchStream.filter((Example)example); + } + + @Override + public FetchableFluentQuery sortBy(Sort sort) { + searchStream.sorted(sort); + return this; + } + + @Override + public FetchableFluentQuery as(Class resultType) { + return new RedisFluentQueryByExample<>(example, sort, domainType, resultType, this.entityStream, this.searchOps, searchStream, mappingContext); + } + + @Override + public FetchableFluentQuery project(Collection properties) { + List> metamodelFields = MetamodelUtils.getMetamodelFieldsForProperties(resultType, properties); + metamodelFields.forEach(mmf -> searchStream.project((MetamodelField) mmf)); + return this; + } + + @Nullable + @Override + public R oneValue() { + Iterator iterator = !isProjection ? searchStream.iterator() : (Iterator) parentSearchStream.iterator(); + + R result = null; + if (iterator.hasNext()) { + result = iterator.next(); + if (iterator.hasNext()) { + throw new IncorrectResultSizeDataAccessException("Query returned non unique result", 1); + } + } + + return !isProjection ? result : conversionFunction.apply(result); + } + + @Nullable + @Override + public R firstValue() { + var result = !isProjection ? searchStream.findFirst().orElse(null) : parentSearchStream.findFirst().orElse(null); + return !isProjection ? (R) result : conversionFunction.apply(result); + } + + @Override + public List all() { + if (!isProjection) { + return searchStream.collect(Collectors.toList()); + } else { + return parentSearchStream.collect(Collectors.toList()).stream().map(this.conversionFunction).toList(); + } + } + + @Override + public Page page(Pageable pageable) { + Assert.notNull(pageable, "Pageable must not be null"); + + Query query = (searchStream.backingQuery().isBlank()) ? new Query() : new Query(searchStream.backingQuery()); + query.limit(0, 0); + SearchResult searchResult = searchOps.search(query); + var count = searchResult.getTotalResults(); + var pageContents = searchStream.limit(pageable.getPageSize()).skip(pageable.getOffset()) + .collect(Collectors.toList()); + return new PageImpl<>(pageContents, pageable, count); + } + + @Override + public Stream stream() { + return all().stream(); + } + + @Override + public long count() { + return searchStream.count(); + } + + @Override + public boolean exists() { + return count() > 0; + } + + private

Function getConversionFunction(Class inputType, Class

targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(targetType, + this.mappingContext, entityInstantiators); + + return o -> (P) converter.convert(o); + } +} diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/ReturnFieldsSearchStreamImpl.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/ReturnFieldsSearchStreamImpl.java index 5c5149a5..665ec109 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/ReturnFieldsSearchStreamImpl.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/ReturnFieldsSearchStreamImpl.java @@ -134,6 +134,11 @@ public SearchStream map(Function mapper) { return new WrapperSearchStream<>(resolveStream()).map(mapper); } + @Override + public Class getEntityClass() { + return null; + } + @Override public IntStream mapToInt(ToIntFunction mapper) { return resolveStream().mapToInt(mapper); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStream.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStream.java index ac261845..e4e17d25 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStream.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStream.java @@ -30,6 +30,8 @@ public interface SearchStream extends BaseStream> { SearchStream map(Function field); + Class getEntityClass(); + Stream map(ToLongFunction mapper); IntStream mapToInt(ToIntFunction mapper); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java index f40ef5e5..d2a12dd6 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java @@ -556,6 +556,7 @@ private Stream resolveStream() { return resolvedStream; } + @Override public Class getEntityClass() { return entityClass; } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/WrapperSearchStream.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/WrapperSearchStream.java index 0db66591..697ed5d7 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/WrapperSearchStream.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/WrapperSearchStream.java @@ -100,6 +100,11 @@ public SearchStream map(Function mapper) { return new WrapperSearchStream<>(backingStream.map(mapper)); } + @Override + public Class getEntityClass() { + return null; + } + @Override public IntStream mapToInt(ToIntFunction mapper) { return backingStream.mapToInt(mapper); diff --git a/redis-om-spring/src/test/java/com/redis/om/spring/AbstractBaseOMTest.java b/redis-om-spring/src/test/java/com/redis/om/spring/AbstractBaseOMTest.java index c21927f8..0b14e06b 100644 --- a/redis-om-spring/src/test/java/com/redis/om/spring/AbstractBaseOMTest.java +++ b/redis-om-spring/src/test/java/com/redis/om/spring/AbstractBaseOMTest.java @@ -1,5 +1,6 @@ package com.redis.om.spring; +import com.google.gson.GsonBuilder; import com.redis.om.spring.indexing.RediSearchIndexer; import com.redis.om.spring.ops.RedisModulesOperations; import com.redis.om.spring.vectorize.FeatureExtractor; @@ -41,6 +42,9 @@ public abstract class AbstractBaseOMTest { protected CustomRedisKeyValueTemplate kvTemplate; @Autowired protected RediSearchIndexer indexer; + @Autowired + @Qualifier("omGsonBuilder") + public GsonBuilder gsonBuilder; protected Comparator closeToComparator = new Comparator() { @Override diff --git a/redis-om-spring/src/test/java/com/redis/om/spring/annotations/document/RedisDocumentQueryByExampleTest.java b/redis-om-spring/src/test/java/com/redis/om/spring/annotations/document/RedisDocumentQueryByExampleTest.java index 68b80c3c..1c1269ee 100644 --- a/redis-om-spring/src/test/java/com/redis/om/spring/annotations/document/RedisDocumentQueryByExampleTest.java +++ b/redis-om-spring/src/test/java/com/redis/om/spring/annotations/document/RedisDocumentQueryByExampleTest.java @@ -132,15 +132,15 @@ void testFindOneByExampleWithExplicitNumericIndexedAnnotation() { @Test void testFindOneByExampleWithFieldWithExplicitGeoIndexedAnnotation() { MyDoc template = new MyDoc(); - template.setLocation(new Point(-122.066540, 37.377690)); + template.setLocation(new Point(-122.124500, 47.640160)); Example example = Example.of(template); Optional maybeDoc1 = repository.findOne(example); assertThat(maybeDoc1).isPresent(); MyDoc doc1 = maybeDoc1.get(); - assertThat(doc1.getTitle()).isEqualTo("hello mundo"); - assertThat(doc1.getANumber()).isEqualTo(2); + assertThat(doc1.getTitle()).isEqualTo("hello world"); + assertThat(doc1.getANumber()).isEqualTo(1); } @Test diff --git a/redis-om-spring/src/test/java/com/redis/om/spring/annotations/hash/RedisHashQueryByExampleTest.java b/redis-om-spring/src/test/java/com/redis/om/spring/annotations/hash/RedisHashQueryByExampleTest.java index 0d78a033..5de46cd9 100644 --- a/redis-om-spring/src/test/java/com/redis/om/spring/annotations/hash/RedisHashQueryByExampleTest.java +++ b/redis-om-spring/src/test/java/com/redis/om/spring/annotations/hash/RedisHashQueryByExampleTest.java @@ -428,7 +428,6 @@ void testFindByShouldCount() { @Test void testFindByShouldReportExists() { - MyHash template = new MyHash(); template.setLocation(new Point(-122.066540, 37.377690)); diff --git a/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleDocumentRepositoryIntegrationTests.java b/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleDocumentRepositoryIntegrationTests.java new file mode 100644 index 00000000..4e01a62a --- /dev/null +++ b/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleDocumentRepositoryIntegrationTests.java @@ -0,0 +1,395 @@ +package com.redis.om.spring.repository.support; + +import com.redis.om.spring.AbstractBaseDocumentTest; +import com.redis.om.spring.RedisOMProperties; +import com.redis.om.spring.annotations.Document; +import com.redis.om.spring.annotations.Indexed; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.redis.repository.core.MappingRedisEntityInformation; +import org.springframework.data.redis.repository.support.QueryByExampleRedisExecutor; +import org.springframework.data.repository.query.FluentQuery; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; + +/** + * Port of org.springframework.data.redis.repository.support.QueryByExampleRedisExecutorIntegrationTests + * from Spring Data Redis. + * Integration tests for {@link QueryByExampleRedisExecutor}. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author John Blum + * @author Brian Sam-Bodden + */ +class QueryByExampleDocumentRepositoryIntegrationTests extends AbstractBaseDocumentTest { + + private final RedisMappingContext mappingContext = new RedisMappingContext(); + + private PersonDoc walt, hank, gus; + + private SimpleRedisDocumentRepository repository; + + @BeforeEach + void before() { + repository = new SimpleRedisDocumentRepository<>( + getEntityInformation(PersonDoc.class), // + new KeyValueTemplate(new RedisKeyValueAdapter(template)), // + modulesOperations, // + indexer, // + mappingContext, // + gsonBuilder, // + featureExtractor, + new RedisOMProperties()); + repository.deleteAll(); + + walt = new PersonDoc("Walter", "White"); + walt.setHometown(new City("Albuquerqe")); + + hank = new PersonDoc("Hank", "Schrader"); + hank.setHometown(new City("Albuquerqe")); + + gus = new PersonDoc("Gus", "Fring"); + gus.setHometown(new City("Albuquerqe")); + + repository.saveAll(Arrays.asList(walt, hank, gus)); + } + + @Test + // DATAREDIS-605 + void shouldFindOneByExample() { + Optional result = repository.findOne(Example.of(walt)); + + assertThat(result).contains(walt); + } + + @Test + // DATAREDIS-605 + void shouldThrowExceptionWhenFindOneByExampleReturnsNonUniqueResult() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThatThrownBy(() -> repository.findOne(Example.of(person))).isInstanceOf( + IncorrectResultSizeDataAccessException.class); + } + + @Test + // DATAREDIS-605 + void shouldNotFindOneByExample() { + Optional result = repository.findOne(Example.of(new PersonDoc("Skyler", "White"))); + assertThat(result).isEmpty(); + } + + @Test + // DATAREDIS-605, GH-2880 + void shouldFindAllByExample() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + Iterable result = repository.findAll(Example.of(person)); + assertThat(result).contains(walt, gus, hank); + } + + @Test + // DATAREDIS-605 + void shouldNotSupportFindAllOrdered() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThatThrownBy(() -> repository.findAll(Example.of(person), Sort.by("foo"))).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + // DATAREDIS-605 + void shouldFindAllPagedByExample() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + Page result = repository.findAll(Example.of(person), PageRequest.of(0, 2)); + assertThat(result).hasSize(2); + } + + @Test + // DATAREDIS-605 + void shouldCountCorrectly() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat(repository.count(Example.of(person))).isEqualTo(3); + assertThat(repository.count(Example.of(walt))).isEqualTo(1); + assertThat(repository.count(Example.of(new PersonDoc()))).isEqualTo(3); + assertThat(repository.count(Example.of(new PersonDoc("Foo", "Bar")))).isZero(); + } + + @Test + // DATAREDIS-605 + void shouldReportExistenceCorrectly() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat(repository.exists(Example.of(person))).isTrue(); + assertThat(repository.exists(Example.of(walt))).isTrue(); + assertThat(repository.exists(Example.of(new PersonDoc()))).isTrue(); + assertThat(repository.exists(Example.of(new PersonDoc("Foo", "Bar")))).isFalse(); + } + + @Test + // GH-2150 + void findByShouldFindFirst() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat((Object) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::first)).isNotNull(); + assertThat( + repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).firstValue()).getFirstname()) // + .isEqualTo(walt.getFirstname() // + ); + } + + @Test + // GH-2150 + void findByShouldFindFirstAsDto() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat(repository.findBy(Example.of(walt), it -> it.as(PersonDto.class).firstValue()).getFirstname()).isEqualTo( + walt.getFirstname()); + } + + @Test + // GH-2150 + void findByShouldFindOne() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::one)); + assertThat( + repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).oneValue()).getFirstname()).isEqualTo( + walt.getFirstname()); + } + + @Test + // GH-2150 + void findByShouldFindAll() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat((List) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::all)).hasSize(3); + List people = repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).all()); + assertThat(people).hasSize(1); + assertThat(people).hasOnlyElementsOfType(PersonProjection.class); + } + + @Test + // GH-2150 + void findByShouldFindPage() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + Page result = repository.findBy(Example.of(person), it -> it.page(PageRequest.of(0, 2))); + assertThat(result).hasSize(2); + assertThat(result.getTotalElements()).isEqualTo(3); + } + + @Test + // GH-2150 + void findByShouldFindStream() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + Stream result = repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::stream); + assertThat(result).hasSize(3); + } + + @Test + // GH-2150 + void findByShouldCount() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat((Long) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::count)).isEqualTo(3); + } + + @Test + // GH-2150 + void findByShouldExists() { + PersonDoc person = new PersonDoc(); + person.setHometown(walt.getHometown()); + + assertThat((Boolean) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::exists)).isTrue(); + } + + @SuppressWarnings("unchecked") + private MappingRedisEntityInformation getEntityInformation(Class entityClass) { + return new MappingRedisEntityInformation<>( + (RedisPersistentEntity) mappingContext.getRequiredPersistentEntity(entityClass)); + } + + @Document("dpersons") + static class PersonDoc { + + private @Id String id; + private @Indexed String firstname; + private String lastname; + private @Indexed City hometown; + + PersonDoc() { + } + + PersonDoc(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstname() { + return this.firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return this.lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public City getHometown() { + return this.hometown; + } + + public void setHometown(City hometown) { + this.hometown = hometown; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PersonDoc that)) { + return false; + } + + return Objects.equals(this.getId(), that.getId()) && Objects.equals(this.getFirstname(), + that.getFirstname()) && Objects.equals(this.getLastname(), that.getLastname()) && Objects.equals( + this.getHometown(), that.getHometown()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getFirstname(), getLastname(), getHometown()); + } + } + + static class City { + + private @Indexed String name; + + public City() { + } + + public City(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof City that)) { + return false; + } + + return Objects.equals(this.getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + } + + static class PersonDto { + + private String firstname; + + public String getFirstname() { + return this.firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PersonDoc that)) { + return false; + } + + return Objects.equals(this.getFirstname(), that.getLastname()); + } + + @Override + public int hashCode() { + return Objects.hash(getFirstname()); + } + + @Override + public String toString() { + return getFirstname(); + } + } + + interface PersonProjection { + String getFirstname(); + } +} diff --git a/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleHashRepositoryIntegrationTests.java b/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleHashRepositoryIntegrationTests.java new file mode 100644 index 00000000..b7dc96dd --- /dev/null +++ b/redis-om-spring/src/test/java/com/redis/om/spring/repository/support/QueryByExampleHashRepositoryIntegrationTests.java @@ -0,0 +1,390 @@ +package com.redis.om.spring.repository.support; + +import com.redis.om.spring.AbstractBaseEnhancedRedisTest; +import com.redis.om.spring.RedisOMProperties; +import com.redis.om.spring.annotations.Indexed; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.redis.repository.core.MappingRedisEntityInformation; +import org.springframework.data.redis.repository.support.QueryByExampleRedisExecutor; +import org.springframework.data.repository.query.FluentQuery; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; + +/** + * Port of org.springframework.data.redis.repository.support.QueryByExampleRedisExecutorIntegrationTests + * from Spring Data Redis. + * Integration tests for {@link QueryByExampleRedisExecutor}. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author John Blum + * @author Brian Sam-Bodden + */ +class QueryByExampleHashRepositoryIntegrationTests extends AbstractBaseEnhancedRedisTest { + + private final RedisMappingContext mappingContext = new RedisMappingContext(); + + private Person walt, hank, gus; + + private SimpleRedisEnhancedRepository repository; + + @BeforeEach + void before() { + + repository = new SimpleRedisEnhancedRepository<>(getEntityInformation(Person.class), + new KeyValueTemplate(new RedisKeyValueAdapter(template)), modulesOperations, indexer, featureExtractor, + new RedisOMProperties()); + repository.deleteAll(); + + walt = new Person("Walter", "White"); + walt.setHometown(new City("Albuquerqe")); + + hank = new Person("Hank", "Schrader"); + hank.setHometown(new City("Albuquerqe")); + + gus = new Person("Gus", "Fring"); + gus.setHometown(new City("Albuquerqe")); + + repository.saveAll(Arrays.asList(walt, hank, gus)); + } + + @Test + // DATAREDIS-605 + void shouldFindOneByExample() { + Optional result = repository.findOne(Example.of(walt)); + + assertThat(result).contains(walt); + } + + @Test + // DATAREDIS-605 + void shouldThrowExceptionWhenFindOneByExampleReturnsNonUniqueResult() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThatThrownBy(() -> repository.findOne(Example.of(person))).isInstanceOf( + IncorrectResultSizeDataAccessException.class); + } + + @Test + // DATAREDIS-605 + void shouldNotFindOneByExample() { + Optional result = repository.findOne(Example.of(new Person("Skyler", "White"))); + assertThat(result).isEmpty(); + } + + @Test + // DATAREDIS-605, GH-2880 + void shouldFindAllByExample() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Iterable result = repository.findAll(Example.of(person)); + assertThat(result).contains(walt, gus, hank); + } + + @Test + // DATAREDIS-605 + void shouldNotSupportFindAllOrdered() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThatThrownBy(() -> repository.findAll(Example.of(person), Sort.by("foo"))).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + // DATAREDIS-605 + void shouldFindAllPagedByExample() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Page result = repository.findAll(Example.of(person), PageRequest.of(0, 2)); + assertThat(result).hasSize(2); + } + + @Test + // DATAREDIS-605 + void shouldCountCorrectly() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat(repository.count(Example.of(person))).isEqualTo(3); + assertThat(repository.count(Example.of(walt))).isEqualTo(1); + assertThat(repository.count(Example.of(new Person()))).isEqualTo(3); + assertThat(repository.count(Example.of(new Person("Foo", "Bar")))).isZero(); + } + + @Test + // DATAREDIS-605 + void shouldReportExistenceCorrectly() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat(repository.exists(Example.of(person))).isTrue(); + assertThat(repository.exists(Example.of(walt))).isTrue(); + assertThat(repository.exists(Example.of(new Person()))).isTrue(); + assertThat(repository.exists(Example.of(new Person("Foo", "Bar")))).isFalse(); + } + + @Test + // GH-2150 + void findByShouldFindFirst() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Object) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::first)).isNotNull(); + assertThat( + repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).firstValue()).getFirstname()) // + .isEqualTo(walt.getFirstname() // + ); + } + + @Test + // GH-2150 + void findByShouldFindFirstAsDto() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat(repository.findBy(Example.of(walt), it -> it.as(PersonDto.class).firstValue()).getFirstname()).isEqualTo( + walt.getFirstname()); + } + + @Test + // GH-2150 + void findByShouldFindOne() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy( + () -> repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::one)); + assertThat( + repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).oneValue()).getFirstname()).isEqualTo( + walt.getFirstname()); + } + + @Test + // GH-2150 + void findByShouldFindAll() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((List) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::all)).hasSize(3); + List people = repository.findBy(Example.of(walt), it -> it.as(PersonProjection.class).all()); + assertThat(people).hasSize(1); + assertThat(people).hasOnlyElementsOfType(PersonProjection.class); + } + + @Test + // GH-2150 + void findByShouldFindPage() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Page result = repository.findBy(Example.of(person), it -> it.page(PageRequest.of(0, 2))); + assertThat(result).hasSize(2); + assertThat(result.getTotalElements()).isEqualTo(3); + } + + @Test + // GH-2150 + void findByShouldFindStream() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Stream result = repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::stream); + assertThat(result).hasSize(3); + } + + @Test + // GH-2150 + void findByShouldCount() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Long) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::count)).isEqualTo(3); + } + + @Test + // GH-2150 + void findByShouldExists() { + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Boolean) repository.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::exists)).isTrue(); + } + + @SuppressWarnings("unchecked") + private MappingRedisEntityInformation getEntityInformation(Class entityClass) { + return new MappingRedisEntityInformation<>( + (RedisPersistentEntity) mappingContext.getRequiredPersistentEntity(entityClass)); + } + + @RedisHash("persons") + static class Person { + + private @Id String id; + private @Indexed String firstname; + private String lastname; + private @Indexed City hometown; + + Person() { + } + + Person(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstname() { + return this.firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return this.lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public City getHometown() { + return this.hometown; + } + + public void setHometown(City hometown) { + this.hometown = hometown; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Person that)) { + return false; + } + + return Objects.equals(this.getId(), that.getId()) && Objects.equals(this.getFirstname(), + that.getFirstname()) && Objects.equals(this.getLastname(), that.getLastname()) && Objects.equals( + this.getHometown(), that.getHometown()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getFirstname(), getLastname(), getHometown()); + } + } + + static class City { + + private @Indexed String name; + + public City() { + } + + public City(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof City that)) { + return false; + } + + return Objects.equals(this.getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + } + + static class PersonDto { + + private String firstname; + + public String getFirstname() { + return this.firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Person that)) { + return false; + } + + return Objects.equals(this.getFirstname(), that.getLastname()); + } + + @Override + public int hashCode() { + return Objects.hash(getFirstname()); + } + + @Override + public String toString() { + return getFirstname(); + } + } + + interface PersonProjection { + String getFirstname(); + } +}