diff --git a/pom.xml b/pom.xml index 373e1f410f..48e9cf60f9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-redis - 2.6.0-SNAPSHOT + 2.6.0-2150-SNAPSHOT Spring Data Redis @@ -137,6 +139,12 @@ true + + org.springframework.data + spring-data-commons + 2.6.0-2228-SNAPSHOT + + diff --git a/src/main/java/org/springframework/data/redis/repository/support/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/redis/repository/support/DtoInstantiatingConverter.java new file mode 100644 index 0000000000..4c9e5ecd89 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/repository/support/DtoInstantiatingConverter.java @@ -0,0 +1,103 @@ +/* + * Copyright 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.redis.repository.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.redis.core.mapping.RedisPersistentEntity; +import org.springframework.data.redis.core.mapping.RedisPersistentProperty; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Mark Paluch + */ +class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityInstantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, RedisPersistentProperty> context, + EntityInstantiators entityInstantiators) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = entityInstantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + PreferredConstructor> constructor = targetEntity.getPersistenceConstructor(); + + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties((SimplePropertyHandler) property -> { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + }); + + return dto; + } +} diff --git a/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java b/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java index c41fb3eac0..b66132a0d3 100644 --- a/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java +++ b/src/main/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutor.java @@ -15,25 +15,38 @@ */ package org.springframework.data.redis.repository.support; -import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Optional; - +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.redis.core.RedisKeyValueTemplate; import org.springframework.data.redis.core.convert.IndexResolver; import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.repository.query.ExampleQueryMapper; import org.springframework.data.redis.repository.query.RedisOperationChain; import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.data.util.Streamable; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,11 +60,14 @@ * @since 2.1 */ @SuppressWarnings("unchecked") -public class QueryByExampleRedisExecutor implements QueryByExampleExecutor { +public class QueryByExampleRedisExecutor + implements QueryByExampleExecutor, BeanFactoryAware, BeanClassLoaderAware { private final EntityInformation entityInformation; private final RedisKeyValueTemplate keyValueTemplate; private final ExampleQueryMapper mapper; + private final SpelAwareProxyProjectionFactory projectionFactory; + private final EntityInstantiators entityInstantiators = new EntityInstantiators(); /** * Create a new {@link QueryByExampleRedisExecutor} given {@link EntityInformation} and {@link RedisKeyValueTemplate}. @@ -85,6 +101,17 @@ public QueryByExampleRedisExecutor(EntityInformation entityInformation, Re this.keyValueTemplate = keyValueTemplate; this.mapper = new ExampleQueryMapper(keyValueTemplate.getMappingContext(), indexResolver); + this.projectionFactory = new SpelAwareProxyProjectionFactory(); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.projectionFactory.setBeanFactory(beanFactory); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.projectionFactory.setBeanClassLoader(classLoader); } /* @@ -94,21 +121,32 @@ public QueryByExampleRedisExecutor(EntityInformation entityInformation, Re @Override public Optional findOne(Example example) { - RedisOperationChain operationChain = createQuery(example); + return Optional.ofNullable(doFindOne(example)); + } - KeyValueQuery query = new KeyValueQuery<>(operationChain); - Iterator iterator = keyValueTemplate.find(query.limit(2), entityInformation.getJavaType()).iterator(); + @Nullable + private S doFindOne(Example example) { - Optional result = Optional.empty(); + Iterator iterator = doFind(example); if (iterator.hasNext()) { - result = Optional.of((S) iterator.next()); + S result = iterator.next(); if (iterator.hasNext()) { throw new IncorrectResultSizeDataAccessException(1); } + + return result; } - return result; + return null; + } + + private Iterator doFind(Example example) { + + RedisOperationChain operationChain = createQuery(example); + + KeyValueQuery query = new KeyValueQuery<>(operationChain); + return (Iterator) keyValueTemplate.find(query.limit(2), entityInformation.getJavaType()).iterator(); } /* @@ -144,19 +182,13 @@ public Page findAll(Example example, Pageable pageable) { RedisOperationChain operationChain = createQuery(example); KeyValueQuery query = new KeyValueQuery<>(operationChain); - Iterable result = keyValueTemplate.find( + List result = (List) keyValueTemplate.find( query.orderBy(pageable.getSort()).skip(pageable.getOffset()).limit(pageable.getPageSize()), entityInformation.getJavaType()); - long count = operationChain.isEmpty() ? keyValueTemplate.count(entityInformation.getJavaType()) - : keyValueTemplate.count(query, entityInformation.getJavaType()); - - List list = new ArrayList<>(); - for (T t : result) { - list.add((S) t); - } - - return new PageImpl<>(list, pageable, count); + return PageableExecutionUtils.getPage(result, pageable, + () -> operationChain.isEmpty() ? keyValueTemplate.count(entityInformation.getJavaType()) + : keyValueTemplate.count(query, entityInformation.getJavaType())); } /* @@ -180,10 +212,180 @@ public boolean exists(Example example) { return count(example) > 0; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findBy(org.springframework.data.domain.Example, java.util.function.Function) + */ + @Override + public R findBy(Example example, + Function, R> queryFunction) { + + Assert.notNull(example, "Example must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + + return queryFunction.apply(new FluentQueryByExample<>(example, example.getProbeType())); + } + private RedisOperationChain createQuery(Example example) { Assert.notNull(example, "Example must not be null!"); return mapper.getMappedExample(example); } + + /** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. + * + * @author Mark Paluch + * @since 2.6 + */ + class FluentQueryByExample implements FluentQuery.FetchableFluentQuery { + + private final Example example; + private final Sort sort; + private final Class domainType; + private final Class resultType; + + FluentQueryByExample(Example example, Class resultType) { + this(example, Sort.unsorted(), resultType, resultType); + } + + FluentQueryByExample(Example example, Sort sort, Class domainType, Class resultType) { + this.example = example; + this.sort = sort; + this.domainType = domainType; + this.resultType = resultType; + } + + @Override + public FetchableFluentQuery sortBy(Sort sort) { + return new FluentQueryByExample<>(example, sort, domainType, resultType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class) + */ + @Override + public FetchableFluentQuery as(Class resultType) { + return new FluentQueryByExample<>(example, sort, domainType, resultType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection) + */ + @Override + public FetchableFluentQuery project(Collection properties) { + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Nullable + @Override + public R oneValue() { + + S one = doFindOne(example); + + if (one != null) { + return getConversionFunction(entityInformation.getJavaType(), resultType).apply(one); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Nullable + @Override + public R firstValue() { + + Iterator iterator = doFind(example); + + if (iterator.hasNext()) { + return getConversionFunction(entityInformation.getJavaType(), resultType).apply(iterator.next()); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + return stream().collect(Collectors.toList()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + Function conversionFunction = getConversionFunction(entityInformation.getJavaType(), resultType); + + List content = findAll(example, pageable).stream().map(conversionFunction).collect(Collectors.toList()); + return PageableExecutionUtils.getPage(content, pageable, this::count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + + Function conversionFunction = getConversionFunction(entityInformation.getJavaType(), resultType); + + if (sort.isSorted()) { + return findAll(example, PageRequest.of(0, Integer.MAX_VALUE, sort)).stream().map(conversionFunction); + } + + return Streamable.of(findAll(example)).map(conversionFunction).stream(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return QueryByExampleRedisExecutor.this.count(example); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return QueryByExampleRedisExecutor.this.exists(example); + } + + 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, + keyValueTemplate.getMappingContext(), entityInstantiators); + + return o -> (P) converter.convert(o); + } + } } diff --git a/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java index 4d016203be..d4717ebb17 100644 --- a/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/support/QueryByExampleRedisExecutorIntegrationTests.java @@ -22,9 +22,10 @@ import lombok.NoArgsConstructor; import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.stream.Stream; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,6 +49,7 @@ import org.springframework.data.redis.core.mapping.RedisPersistentEntity; import org.springframework.data.redis.repository.core.MappingRedisEntityInformation; import org.springframework.data.redis.test.extension.RedisStanalone; +import org.springframework.data.repository.query.FluentQuery; /** * Integration tests for {@link QueryByExampleRedisExecutor}. @@ -197,6 +199,113 @@ void shouldReportExistenceCorrectly() { assertThat(executor.exists(Example.of(new Person("Foo", "Bar")))).isFalse(); } + @Test // GH-2150 + void findByShouldFindFirst() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Object) executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::first)).isNotNull(); + assertThat(executor.findBy(Example.of(walt), it -> it.as(PersonProjection.class).firstValue()).getFirstname()) + .isEqualTo(walt.getFirstname()); + } + + @Test // GH-2150 + void findByShouldFindFirstAsDto() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat(executor.findBy(Example.of(walt), it -> it.as(PersonDto.class).firstValue()).getFirstname()) + .isEqualTo(walt.getFirstname()); + } + + @Test // GH-2150 + void findByShouldFindOne() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::one)); + assertThat(executor.findBy(Example.of(walt), it -> it.as(PersonProjection.class).oneValue()).getFirstname()) + .isEqualTo(walt.getFirstname()); + } + + @Test // GH-2150 + void findByShouldFindAll() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((List) executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::all)).hasSize(3); + List people = executor.findBy(Example.of(walt), it -> it.as(PersonProjection.class).all()); + assertThat(people).hasOnlyElementsOfType(PersonProjection.class); + } + + @Test // GH-2150 + void findByShouldFindPage() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Page result = executor.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() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + Stream result = executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::stream); + assertThat(result).hasSize(3); + } + + @Test // GH-2150 + void findByShouldCount() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Long) executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::count)).isEqualTo(3); + } + + @Test // GH-2150 + void findByShouldExists() { + + QueryByExampleRedisExecutor executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class), + kvTemplate); + + Person person = new Person(); + person.setHometown(walt.getHometown()); + + assertThat((Boolean) executor.findBy(Example.of(person), FluentQuery.FetchableFluentQuery::exists)).isTrue(); + } + @SuppressWarnings("unchecked") private MappingRedisEntityInformation getEntityInformation(Class entityClass) { return new MappingRedisEntityInformation<>( @@ -227,4 +336,14 @@ static class Person { static class City { @Indexed String name; } + + @Data + static class PersonDto { + String firstname; + } + + interface PersonProjection { + + String getFirstname(); + } }