diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index 875f61abe..1fae30fda 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -1,4 +1,4 @@ -[[cassandra.projections]] +[[redis.projections]] = Projections include::{commons}@data-commons::page$repositories/projections.adoc[leveloffset=+1] diff --git a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java index 963184e29..d5b122410 100644 --- a/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java +++ b/src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java @@ -46,6 +46,7 @@ import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProv import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.repository.query.RedisPartTreeQuery; import org.springframework.data.redis.repository.query.RedisQueryCreator; import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; import org.springframework.lang.Nullable; @@ -106,15 +107,15 @@ public class RedisRuntimeHints implements RuntimeHintsRegistrar { TypeReference.of(ReactiveClusterScriptingCommands.class), TypeReference.of(ReactiveClusterGeoCommands.class), TypeReference.of(ReactiveClusterHyperLogLogCommands.class), TypeReference.of(ReactiveRedisOperations.class), - TypeReference.of(ReactiveRedisConnectionFactory.class), - TypeReference.of(ReactiveRedisTemplate.class), TypeReference.of(RedisOperations.class), - TypeReference.of(RedisTemplate.class), TypeReference.of(StringRedisTemplate.class), - TypeReference.of(KeyspaceConfiguration.class), TypeReference.of(MappingConfiguration.class), - TypeReference.of(MappingRedisConverter.class), TypeReference.of(RedisConverter.class), - TypeReference.of(RedisCustomConversions.class), TypeReference.of(ReferenceResolver.class), - TypeReference.of(ReferenceResolverImpl.class), TypeReference.of(IndexConfiguration.class), - TypeReference.of(ConfigurableIndexDefinitionProvider.class), TypeReference.of(RedisMappingContext.class), - TypeReference.of(RedisRepositoryFactoryBean.class), TypeReference.of(RedisQueryCreator.class), + TypeReference.of(ReactiveRedisConnectionFactory.class), TypeReference.of(ReactiveRedisTemplate.class), + TypeReference.of(RedisOperations.class), TypeReference.of(RedisTemplate.class), + TypeReference.of(StringRedisTemplate.class), TypeReference.of(KeyspaceConfiguration.class), + TypeReference.of(MappingConfiguration.class), TypeReference.of(MappingRedisConverter.class), + TypeReference.of(RedisConverter.class), TypeReference.of(RedisCustomConversions.class), + TypeReference.of(ReferenceResolver.class), TypeReference.of(ReferenceResolverImpl.class), + TypeReference.of(IndexConfiguration.class), TypeReference.of(ConfigurableIndexDefinitionProvider.class), + TypeReference.of(RedisMappingContext.class), TypeReference.of(RedisRepositoryFactoryBean.class), + TypeReference.of(RedisQueryCreator.class), TypeReference.of(RedisPartTreeQuery.class), TypeReference.of(MessageListener.class), TypeReference.of(RedisMessageListenerContainer.class), TypeReference diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index d738a1e95..e9e1a9f31 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -16,8 +16,17 @@ package org.springframework.data.redis.core.convert; import java.lang.reflect.Array; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1046,6 +1055,11 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { return this.indexResolver; } + @Override + public EntityInstantiators getEntityInstantiators() { + return entityInstantiators; + } + @Override public ConversionService getConversionService() { return this.conversionService; diff --git a/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java index e52d942bc..998f22323 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core.convert; import org.springframework.data.convert.EntityConverter; +import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.core.mapping.RedisPersistentEntity; import org.springframework.data.redis.core.mapping.RedisPersistentProperty; @@ -40,4 +41,10 @@ public interface RedisConverter */ @Nullable IndexResolver getIndexResolver(); + + /** + * @return the configured {@link EntityInstantiators}. + * @since 3.2.4 + */ + EntityInstantiators getEntityInstantiators(); } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java index d23692204..eea89db92 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java @@ -33,6 +33,7 @@ import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; +import org.springframework.data.redis.repository.query.RedisPartTreeQuery; import org.springframework.data.redis.repository.query.RedisQueryCreator; import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; @@ -52,7 +53,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; @Documented @Inherited @Import(RedisRepositoriesRegistrar.class) -@QueryCreatorType(RedisQueryCreator.class) +@QueryCreatorType(value = RedisQueryCreator.class, repositoryQueryType = RedisPartTreeQuery.class) public @interface EnableRedisRepositories { /** diff --git a/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java b/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java new file mode 100644 index 000000000..8ba04ac02 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/query/RedisPartTreeQuery.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 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.redis.repository.query; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.DtoInstantiatingConverter; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.redis.core.RedisKeyValueAdapter; +import org.springframework.data.redis.core.convert.RedisConverter; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Redis-specific implementation of {@link KeyValuePartTreeQuery} supporting projections. + * + * @author Mark Paluch + * @since 3.2.4 + */ +public class RedisPartTreeQuery extends KeyValuePartTreeQuery { + + private final RedisKeyValueAdapter adapter; + + public RedisPartTreeQuery(QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider, + KeyValueOperations template, Class> queryCreator) { + super(queryMethod, evaluationContextProvider, template, queryCreator); + this.adapter = (RedisKeyValueAdapter) template.getKeyValueAdapter(); + } + + @Override + public Object execute(Object[] parameters) { + + ParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters); + KeyValueQuery query = prepareQuery(parameters); + ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + + RedisConverter converter = adapter.getConverter(); + Converter resultPostProcessor = new ResultProcessingConverter(processor, + converter.getMappingContext(), converter.getEntityInstantiators()); + + Object source = doExecute(parameters, query); + return source != null ? processor.processResult(resultPostProcessor.convert(source)) : null; + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Mark Paluch + */ + static final class ResultProcessingConverter implements Converter { + + private final ResultProcessor processor; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiators instantiators; + + public ResultProcessingConverter(ResultProcessor processor, + MappingContext, ? extends PersistentProperty> context, + EntityInstantiators instantiators) { + + Assert.notNull(processor, "Processor must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(instantiators, "Instantiators must not be null!"); + + this.processor = processor; + this.context = context; + this.instantiators = instantiators; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (source instanceof Set s) { + + Set target = new LinkedHashSet<>(s.size()); + + for (Object o : s) { + target.add(convert(o)); + } + + return target; + } + + if (source instanceof Collection c) { + + List target = new ArrayList<>(c.size()); + + for (Object o : c) { + target.add(convert(o)); + } + + return target; + } + + if (source instanceof Streamable s) { + return s.map(this::convert); + } + + ReturnedType returnedType = processor.getReturnedType(); + + if (ReflectionUtils.isVoid(returnedType.getReturnedType())) { + return null; + } + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), context, + instantiators); + + return processor.processResult(source, converter); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java index f96041125..5197d7665 100644 --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java @@ -16,11 +16,11 @@ package org.springframework.data.redis.repository.support; import org.springframework.data.keyvalue.core.KeyValueOperations; -import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; 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.query.RedisPartTreeQuery; import org.springframework.data.redis.repository.query.RedisQueryCreator; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -59,7 +59,7 @@ public class RedisRepositoryFactory extends KeyValueRepositoryFactory { */ public RedisRepositoryFactory(KeyValueOperations keyValueOperations, Class> queryCreator) { - this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class); + this(keyValueOperations, queryCreator, RedisPartTreeQuery.class); } /** diff --git a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java index 448364c41..b0ecb5397 100644 --- a/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.repository.support; import org.springframework.beans.factory.FactoryBean; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; +import org.springframework.data.redis.repository.query.RedisPartTreeQuery; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -44,6 +45,7 @@ public class RedisRepositoryFactoryBean, S, ID> */ public RedisRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); + setQueryType(RedisPartTreeQuery.class); } @Override diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java index 16d796c11..94009df44 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java @@ -51,6 +51,7 @@ import org.springframework.data.redis.core.index.SimpleIndexDefinition; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; /** @@ -130,9 +131,13 @@ public abstract class RedisRepositoryIntegrationTestBase { repo.saveAll(Arrays.asList(rand, egwene)); - PersonProjection projectionById = repo.findProjectionById(rand.getId()); - assertThat(projectionById).isNotNull(); - assertThat(projectionById.getFirstname()).isEqualTo(rand.firstname); + PersonProjection projection = repo.findProjectionById(rand.getId(), PersonProjection.class); + assertThat(projection).isNotNull(); + assertThat(projection.getFirstname()).isEqualTo(rand.firstname); + + PersonDto dto = repo.findProjectionById(rand.getId(), PersonDto.class); + assertThat(dto).isNotNull(); + assertThat(dto.firstname()).isEqualTo(rand.firstname); } @Test // GH-2851 @@ -147,10 +152,20 @@ public abstract class RedisRepositoryIntegrationTestBase { repo.saveAll(Arrays.asList(rand, egwene)); - List projectionById = repo.findProjectionBy(); - assertThat(projectionById).hasSize(2) // + List projection = repo.findProjectionBy(); + assertThat(projection).hasSize(2) // .extracting(PersonProjection::getFirstname) // .contains(rand.getFirstname(), egwene.getFirstname()); + + projection = repo.findProjectionStreamBy().toList(); + assertThat(projection).hasSize(2) // + .extracting(PersonProjection::getFirstname) // + .contains(rand.getFirstname(), egwene.getFirstname()); + + List dtos = repo.findProjectionDtoBy(); + assertThat(dtos).hasSize(2) // + .extracting(PersonDto::firstname) // + .contains(rand.getFirstname(), egwene.getFirstname()); } @Test // DATAREDIS-425 @@ -624,10 +639,14 @@ public abstract class RedisRepositoryIntegrationTestBase { Person findEntityById(String id); - PersonProjection findProjectionById(String id); + T findProjectionById(String id, Class projection); + + Streamable findProjectionStreamBy(); List findProjectionBy(); + List findProjectionDtoBy(); + @Override List findAll(Example example); } @@ -636,6 +655,9 @@ public abstract class RedisRepositoryIntegrationTestBase { String getFirstname(); } + record PersonDto(String firstname) { + } + public interface CityRepository extends CrudRepository { List findByLocationNear(Point point, Distance distance);