Last active
September 24, 2025 11:14
-
-
Save thomasdarimont/c59ac8679aae04be8ce0cd73af10b59e to your computer and use it in GitHub Desktop.
Example PoC for class based Projections with Spring Data Repositories
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package demo; | |
| import java.util.List; | |
| import javax.persistence.Entity; | |
| import javax.persistence.GeneratedValue; | |
| import javax.persistence.Id; | |
| import org.springframework.boot.CommandLineRunner; | |
| import org.springframework.boot.SpringApplication; | |
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| import org.springframework.context.annotation.Bean; | |
| import org.springframework.data.jpa.repository.JpaRepository; | |
| import lombok.AllArgsConstructor; | |
| import lombok.Data; | |
| import lombok.NoArgsConstructor; | |
| @SpringBootApplication | |
| public class ExampleApp { | |
| public static void main(String[] args) { | |
| SpringApplication.run(ExampleApp.class, args); | |
| } | |
| @Bean | |
| public CommandLineRunner clr(CustomerRepository repo) { | |
| return (args) -> { | |
| Customer customer = new Customer(); | |
| customer.setFirstname("Franz"); | |
| customer.setLastname("Kafka"); | |
| repo.save(customer); | |
| List<CustomerInfo> customerInfos = repo.findAllCustomerInfosBy(); | |
| System.out.println(customerInfos); | |
| List<PersonName> customerNames = repo.findAllCustomerNamesBy(); | |
| System.out.println(customerNames); | |
| }; | |
| } | |
| } | |
| interface CustomerRepository extends JpaRepository<Customer, Long> { | |
| List<CustomerInfo> findAllCustomerInfosBy(); | |
| List<PersonName> findAllCustomerNamesBy(); | |
| } | |
| @Entity | |
| @Data | |
| @NoArgsConstructor | |
| @AllArgsConstructor | |
| class Customer { | |
| @Id | |
| @GeneratedValue | |
| Long id; | |
| String firstname; | |
| String lastname; | |
| } | |
| interface CustomerInfo { | |
| Long getId(); | |
| String getFirstname(); | |
| String getLastname(); | |
| } | |
| @Data | |
| class PersonName { | |
| String firstname; | |
| String lastname; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package demo; | |
| import java.beans.PropertyDescriptor; | |
| import java.util.Collections; | |
| import java.util.Set; | |
| import javax.persistence.Entity; | |
| import org.springframework.beans.BeanUtils; | |
| import org.springframework.beans.BeanWrapperImpl; | |
| import org.springframework.core.convert.TypeDescriptor; | |
| import org.springframework.core.convert.converter.ConditionalConverter; | |
| import org.springframework.core.convert.converter.GenericConverter; | |
| import org.springframework.stereotype.Component; | |
| @Component | |
| public class FieldMappingObjectConverter implements GenericConverter, ConditionalConverter { | |
| @Override | |
| public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { | |
| //only for entities? | |
| //TODO check if source type is a entity managed by Spring Data | |
| if (!sourceType.hasAnnotation(Entity.class)) { | |
| return false; | |
| } | |
| if (targetType.getType().isInterface()) { | |
| return false; | |
| } | |
| return isCompatibleProjectionType(sourceType, targetType); | |
| } | |
| private boolean isCompatibleProjectionType(TypeDescriptor sourceType, TypeDescriptor targetType) { | |
| // TODO perform more sophisticated compatibility check, e.g. take matching fields into account, or mapping annotations... | |
| PropertyDescriptor[] targetPds = new BeanWrapperImpl(targetType.getClass()).getPropertyDescriptors(); | |
| PropertyDescriptor[] sourcePds = new BeanWrapperImpl(sourceType.getClass()).getPropertyDescriptors(); | |
| for (PropertyDescriptor pdt : targetPds) { | |
| boolean found = false; | |
| for (PropertyDescriptor pds : sourcePds) { | |
| found |= pdt.getName().equals(pds.getName()); | |
| } | |
| if (!found) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| @Override | |
| public Set<ConvertiblePair> getConvertibleTypes() { | |
| return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); | |
| } | |
| @Override | |
| public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { | |
| //TODO more sophisticated bean creation and copying... | |
| Object target = BeanUtils.instantiateClass(targetType.getObjectType()); | |
| BeanUtils.copyProperties(source, target); | |
| return target; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Copyright 2015-2016 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 | |
| * | |
| * http://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.repository.query; | |
| import lombok.NonNull; | |
| import lombok.RequiredArgsConstructor; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.function.Function; | |
| import java.util.stream.Stream; | |
| import org.springframework.core.CollectionFactory; | |
| import org.springframework.core.convert.ConversionService; | |
| import org.springframework.core.convert.converter.Converter; | |
| import org.springframework.core.convert.support.DefaultConversionService; | |
| import org.springframework.data.domain.Slice; | |
| import org.springframework.data.projection.ProjectionFactory; | |
| import org.springframework.data.util.ReflectionUtils; | |
| import org.springframework.util.Assert; | |
| import demo.FieldMappingObjectConverter; | |
| /** | |
| * A {@link ResultProcessor} to expose metadata about query result element projection and eventually post prcessing raw | |
| * query results into projections and data transfer objects. | |
| * | |
| * @author Oliver Gierke | |
| * @author John Blum | |
| * @since 1.12 | |
| */ | |
| public class ResultProcessor { | |
| private final QueryMethod method; | |
| private final ProjectingConverter converter; | |
| private final ProjectionFactory factory; | |
| private ReturnedType type; | |
| /** | |
| * Creates a new {@link ResultProcessor} from the given {@link QueryMethod} and {@link ProjectionFactory}. | |
| * | |
| * @param method must not be {@literal null}. | |
| * @param factory must not be {@literal null}. | |
| */ | |
| ResultProcessor(QueryMethod method, ProjectionFactory factory) { | |
| this(method, factory, method.getReturnedObjectType()); | |
| } | |
| /** | |
| * Creates a new {@link ResultProcessor} for the given {@link QueryMethod}, {@link ProjectionFactory} and type. | |
| * | |
| * @param method must not be {@literal null}. | |
| * @param factory must not be {@literal null}. | |
| * @param type must not be {@literal null}. | |
| */ | |
| private ResultProcessor(QueryMethod method, ProjectionFactory factory, Class<?> type) { | |
| Assert.notNull(method, "QueryMethod must not be null!"); | |
| Assert.notNull(factory, "ProjectionFactory must not be null!"); | |
| Assert.notNull(type, "Type must not be null!"); | |
| this.method = method; | |
| this.type = ReturnedType.of(type, method.getDomainClass(), factory); | |
| this.converter = new ProjectingConverter(this.type, factory); | |
| this.factory = factory; | |
| } | |
| /** | |
| * Returns a new {@link ResultProcessor} with a new projection type obtained from the given {@link ParameterAccessor}. | |
| * | |
| * @param accessor can be {@literal null}. | |
| * @return | |
| */ | |
| public ResultProcessor withDynamicProjection(ParameterAccessor accessor) { | |
| if (accessor == null) { | |
| return this; | |
| } | |
| Class<?> projectionType = accessor.getDynamicProjection(); | |
| return projectionType == null ? this : new ResultProcessor(method, factory, projectionType); | |
| } | |
| /** | |
| * Returns the {@link ReturnedType}. | |
| * | |
| * @return | |
| */ | |
| public ReturnedType getReturnedType() { | |
| return type; | |
| } | |
| /** | |
| * Post-processes the given query result. | |
| * | |
| * @param source can be {@literal null}. | |
| * @return | |
| */ | |
| public <T> T processResult(Object source) { | |
| return processResult(source, NoOpConverter.INSTANCE); | |
| } | |
| /** | |
| * Post-processes the given query result using the given preparing {@link Converter} to potentially prepare collection | |
| * elements. | |
| * | |
| * @param source can be {@literal null}. | |
| * @param preparingConverter must not be {@literal null}. | |
| * @return | |
| */ | |
| @SuppressWarnings("unchecked") | |
| public <T> T processResult(Object source, Converter<Object, Object> preparingConverter) { | |
| if (source == null || type.isInstance(source) || !type.isProjecting()) { | |
| return (T) source; | |
| } | |
| Assert.notNull(preparingConverter, "Preparing converter must not be null!"); | |
| final ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter) | |
| .and(this.converter); | |
| if (source instanceof Slice && method.isPageQuery() || method.isSliceQuery()) { | |
| return (T) ((Slice<?>) source).map(converter); | |
| } | |
| if (source instanceof Collection && method.isCollectionQuery()) { | |
| Collection<?> collection = (Collection<?>) source; | |
| Collection<Object> target = createCollectionFor(collection); | |
| for (Object columns : collection) { | |
| target.add(type.isInstance(columns) ? columns : converter.convert(columns)); | |
| } | |
| return (T) target; | |
| } | |
| if (ReflectionUtils.isJava8StreamType(source.getClass()) && method.isStreamQuery()) { | |
| return (T) new StreamQueryResultHandler(type, converter).handle(source); | |
| } | |
| return (T) converter.convert(source); | |
| } | |
| /** | |
| * Creates a new {@link Collection} for the given source. Will try to create an instance of the source collection's | |
| * type first falling back to creating an approximate collection if the former fails. | |
| * | |
| * @param source must not be {@literal null}. | |
| * @return | |
| */ | |
| private static Collection<Object> createCollectionFor(Collection<?> source) { | |
| try { | |
| return CollectionFactory.createCollection(source.getClass(), source.size()); | |
| } catch (RuntimeException o_O) { | |
| return CollectionFactory.createApproximateCollection(source, source.size()); | |
| } | |
| } | |
| @RequiredArgsConstructor(staticName = "of") | |
| private static class ChainingConverter implements Converter<Object, Object> { | |
| private final @NonNull Class<?> targetType; | |
| private final @NonNull Converter<Object, Object> delegate; | |
| /** | |
| * Returns a new {@link ChainingConverter} that hands the elements resulting from the current conversion to the | |
| * given {@link Converter}. | |
| * | |
| * @param converter must not be {@literal null}. | |
| * @return | |
| */ | |
| public ChainingConverter and(final Converter<Object, Object> converter) { | |
| Assert.notNull(converter, "Converter must not be null!"); | |
| return new ChainingConverter(targetType, new Converter<Object, Object>() { | |
| @Override | |
| public Object convert(Object source) { | |
| Object intermediate = ChainingConverter.this.convert(source); | |
| return targetType.isInstance(intermediate) ? intermediate : converter.convert(intermediate); | |
| } | |
| }); | |
| } | |
| /* | |
| * (non-Javadoc) | |
| * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) | |
| */ | |
| @Override | |
| public Object convert(Object source) { | |
| return delegate.convert(source); | |
| } | |
| } | |
| /** | |
| * A simple {@link Converter} that will return the source value as is. | |
| * | |
| * @author Oliver Gierke | |
| * @since 1.12 | |
| */ | |
| private static enum NoOpConverter implements Converter<Object, Object> { | |
| INSTANCE; | |
| /* | |
| * (non-Javadoc) | |
| * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) | |
| */ | |
| @Override | |
| public Object convert(Object source) { | |
| return source; | |
| } | |
| } | |
| @RequiredArgsConstructor | |
| private static class ProjectingConverter implements Converter<Object, Object> { | |
| private final @NonNull ReturnedType type; | |
| private final @NonNull ProjectionFactory factory; | |
| private final ConversionService conversionService = new DefaultConversionService(); | |
| // XXXXX extension for class based projections | |
| { | |
| ((DefaultConversionService) conversionService).addConverter(new FieldMappingObjectConverter()); | |
| } | |
| /* | |
| * (non-Javadoc) | |
| * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) | |
| */ | |
| @Override | |
| public Object convert(Object source) { | |
| Class<?> targetType = type.getReturnedType(); | |
| if (targetType.isInterface()) { | |
| return factory.createProjection(targetType, getProjectionTarget(source)); | |
| } | |
| return conversionService.convert(source, targetType); | |
| } | |
| private Object getProjectionTarget(Object source) { | |
| if (source != null && source.getClass().isArray()) { | |
| source = Arrays.asList((Object[]) source); | |
| } | |
| if (source instanceof Collection) { | |
| return toMap((Collection<?>) source, type.getInputProperties()); | |
| } | |
| return source; | |
| } | |
| private static Map<String, Object> toMap(Collection<?> values, List<String> names) { | |
| int i = 0; | |
| Map<String, Object> result = new HashMap<String, Object>(values.size()); | |
| for (Object element : values) { | |
| result.put(names.get(i++), element); | |
| } | |
| return result; | |
| } | |
| } | |
| /** | |
| * Handler for Repository query methods returning a Java 8 Stream result by ensuring the {@link Stream} elements match | |
| * the expected return type of the query method. | |
| * | |
| * @author John Blum | |
| * @author Oliver Gierke | |
| */ | |
| @RequiredArgsConstructor | |
| static class StreamQueryResultHandler { | |
| private final @NonNull ReturnedType returnType; | |
| private final @NonNull Converter<Object, Object> converter; | |
| /** | |
| * Processes the given source object as a {@link Stream}, mapping each element to the required return type, | |
| * converting if necessary. | |
| * | |
| * @param source the {@link Stream} of elements to process, must not be {@literal null}. | |
| * @return a new {@link Stream} with the source {@link Stream}'s elements mapped to the target type. | |
| */ | |
| @SuppressWarnings("unchecked") | |
| public Object handle(Object source) { | |
| Assert.isInstanceOf(Stream.class, source, "Source must not be null and an instance of Stream!"); | |
| return ((Stream<Object>) source).map(new Function<Object, Object>() { | |
| @Override | |
| public Object apply(Object element) { | |
| return returnType.isInstance(element) ? element : converter.convert(element); | |
| } | |
| }); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment