Skip to content

Commit

Permalink
Add support for class-based DTOs for Fluent API.
Browse files Browse the repository at this point in the history
Also, interface-based projections now use Tuple queries to consistently use tuple-based queries.

Closes #2327
  • Loading branch information
mp911de authored and christophstrobl committed Dec 4, 2024
1 parent c8bc94d commit 6fe1b2b
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
*/
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);

static class TupleConverter implements Converter<Object, Object> {
public static class TupleConverter implements Converter<Object, Object> {

private final ReturnedType type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,8 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
return toExpressionRecursively(from, property, false);
}

static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
public static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property,
boolean isForSelection) {
return toExpressionRecursively(from, property, isForSelection, false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@
package org.springframework.data.jpa.repository.support;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
Expand All @@ -36,10 +37,18 @@
import org.springframework.data.jpa.repository.query.ScrollDelegate;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.ExpressionBase;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Visitor;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.impl.AbstractJPAQuery;

/**
Expand All @@ -57,33 +66,41 @@
*/
class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> implements FetchableFluentQuery<R> {

private final EntityPath<?> entityPath;
private final JpaEntityInformation<S, ?> entityInformation;
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory;
private final Predicate predicate;
private final Function<Sort, AbstractJPAQuery<?, ?>> finder;

private final PredicateScrollDelegate<S> scroll;
private final BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder;
private final Function<Predicate, Long> countOperation;
private final Function<Predicate, Boolean> existsOperation;
private final EntityManager entityManager;

FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType,
Function<Sort, AbstractJPAQuery<?, ?>> finder, PredicateScrollDelegate<S> scroll,
FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
JpaEntityInformation<S, ?> entityInformation, Function<Sort, AbstractJPAQuery<?, ?>> finder,
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder, Function<Predicate, Long> countOperation,
Function<Predicate, Boolean> existsOperation, EntityManager entityManager, ProjectionFactory projectionFactory) {
this(predicate, entityType, (Class<R>) entityType, Sort.unsorted(), 0, Collections.emptySet(), finder, scroll,
this(entityPath, predicate, entityInformation, (Class<R>) entityInformation.getJavaType(), Sort.unsorted(), 0,
Collections.emptySet(), finder, scrollQueryFactory,
pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
}

private FetchableFluentQueryByPredicate(Predicate predicate, Class<S> entityType, Class<R> resultType, Sort sort,
int limit, Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
PredicateScrollDelegate<S> scroll, BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
private FetchableFluentQueryByPredicate(EntityPath<?> entityPath, Predicate predicate,
JpaEntityInformation<S, ?> entityInformation, Class<R> resultType, Sort sort, int limit,
Collection<String> properties, Function<Sort, AbstractJPAQuery<?, ?>> finder,
ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
BiFunction<Sort, Pageable, AbstractJPAQuery<?, ?>> pagedFinder,
Function<Predicate, Long> countOperation, Function<Predicate, Boolean> existsOperation,
EntityManager entityManager, ProjectionFactory projectionFactory) {

super(resultType, sort, limit, properties, entityType, projectionFactory);
super(resultType, sort, limit, properties, entityInformation.getJavaType(), projectionFactory);
this.entityInformation = entityInformation;
this.entityPath = entityPath;
this.predicate = predicate;
this.finder = finder;
this.scroll = scroll;
this.scrollQueryFactory = scrollQueryFactory;
this.pagedFinder = pagedFinder;
this.countOperation = countOperation;
this.existsOperation = existsOperation;
Expand All @@ -95,37 +112,37 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {

Assert.notNull(sort, "Sort must not be null");

return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, this.sort.and(sort), limit,
properties, finder, scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType,
this.sort.and(sort), limit, properties, finder, scrollQueryFactory, pagedFinder, countOperation,
existsOperation, entityManager, projectionFactory);
}

@Override
public FetchableFluentQuery<R> limit(int limit) {

Assert.isTrue(limit >= 0, "Limit must not be negative");

return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
projectionFactory);
}

@Override
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {

Assert.notNull(resultType, "Projection target type must not be null");

if (!resultType.isInterface()) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}

return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit, properties, finder,
scroll, pagedFinder, countOperation, existsOperation, entityManager, projectionFactory);
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
properties, finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation, entityManager,
projectionFactory);
}

@Override
public FetchableFluentQuery<R> project(Collection<String> properties) {

return new FetchableFluentQueryByPredicate<>(predicate, entityType, resultType, sort, limit,
mergeProperties(properties), finder, scroll, pagedFinder, countOperation, existsOperation, entityManager,
return new FetchableFluentQueryByPredicate<>(entityPath, predicate, entityInformation, resultType, sort, limit,
mergeProperties(properties), finder, scrollQueryFactory, pagedFinder, countOperation, existsOperation,
entityManager,
projectionFactory);
}

Expand Down Expand Up @@ -163,7 +180,8 @@ public Window<R> scroll(ScrollPosition scrollPosition) {

Assert.notNull(scrollPosition, "ScrollPosition must not be null");

return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
return new PredicateScrollDelegate<>(scrollQueryFactory, entityInformation)
.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
}

@Override
Expand Down Expand Up @@ -192,6 +210,35 @@ public boolean exists() {
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {

AbstractJPAQuery<?, ?> query = finder.apply(sort);
applyQuerySettings(this.returnedType, this.limit, query, null);
return query;
}

private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJPAQuery<?, ?> query,
@Nullable ScrollPosition scrollPosition) {

List<String> inputProperties = returnedType.getInputProperties();

if (returnedType.needsCustomConstruction() && !inputProperties.isEmpty()) {

Collection<String> requiredSelection;
if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) {
requiredSelection = new LinkedHashSet<>(inputProperties);
sort.forEach(it -> requiredSelection.add(it.getProperty()));
entityInformation.getIdAttributeNames().forEach(requiredSelection::add);
} else {
requiredSelection = inputProperties;
}

PathBuilder<?> builder = new PathBuilder<>(entityPath.getType(), entityPath.getMetadata());
Expression<?>[] projection = requiredSelection.stream().map(builder::get).toArray(Expression[]::new);

if (returnedType.getReturnedType().isInterface()) {
query.select(new JakartaTuple(projection));
} else {
query.select(new DtoProjection(returnedType.getReturnedType(), projection));
}
}

if (!properties.isEmpty()) {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
Expand All @@ -200,8 +247,6 @@ public boolean exists() {
if (limit != 0) {
query.limit(limit);
}

return query;
}

private Page<R> readPage(Pageable pageable) {
Expand Down Expand Up @@ -233,23 +278,57 @@ private Function<Object, R> getConversionFunction() {
return getConversionFunction(entityType, resultType);
}

static class PredicateScrollDelegate<T> extends ScrollDelegate<T> {
class PredicateScrollDelegate<T> extends ScrollDelegate<T> {

private final ScrollQueryFactory scrollFunction;
private final ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollFunction;

PredicateScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
PredicateScrollDelegate(ScrollQueryFactory<AbstractJPAQuery<?, ?>> scrollQueryFactory,
JpaEntityInformation<T, ?> entity) {
super(entity);
this.scrollFunction = scrollQueryFactory;
}

public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {

Query query = scrollFunction.createQuery(sort, scrollPosition);
if (limit > 0) {
query = query.setMaxResults(limit);
}
return scroll(query, sort, scrollPosition);
AbstractJPAQuery<?, ?> query = scrollFunction.createQuery(returnedType, sort, scrollPosition);

applyQuerySettings(returnedType, limit, query, scrollPosition);

return scroll(query.createQuery(), sort, scrollPosition);
}
}

private static class DtoProjection extends ExpressionBase<Object> {

private final Expression<?>[] projection;

public DtoProjection(Class<?> resultType, Expression<?>[] projection) {
super(resultType);
this.projection = projection;
}

@SuppressWarnings("unchecked")
@Override
public <R, C> R accept(Visitor<R, C> v, @Nullable C context) {

if (v instanceof JPQLSerializer s) {

s.append("new ").append(getType().getName()).append("(");
boolean first = true;
for (Expression<?> expression : projection) {
if (first) {
first = false;
} else {
s.append(", ");
}

expression.accept(v, context);
}

s.append(")");
}

return (R) this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

Expand All @@ -38,6 +39,7 @@
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.Assert;

Expand All @@ -55,13 +57,14 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
implements FluentQuery.FetchableFluentQuery<R> {

private final Specification<S> spec;
private final Function<Sort, TypedQuery<S>> finder;
private final BiFunction<ReturnedType, Sort, TypedQuery<S>> finder;
private final SpecificationScrollDelegate<S> scroll;
private final Function<Specification<S>, Long> countOperation;
private final Function<Specification<S>, Boolean> existsOperation;
private final EntityManager entityManager;

FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Function<Sort, TypedQuery<S>> finder,
FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType,
BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
ProjectionFactory projectionFactory) {
Expand All @@ -70,7 +73,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
}

private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> entityType, Class<R> resultType,
Sort sort, int limit, Collection<String> properties, Function<Sort, TypedQuery<S>> finder,
Sort sort, int limit, Collection<String> properties, BiFunction<ReturnedType, Sort, TypedQuery<S>> finder,
SpecificationScrollDelegate<S> scrollDelegate, Function<Specification<S>, Long> countOperation,
Function<Specification<S>, Boolean> existsOperation, EntityManager entityManager,
ProjectionFactory projectionFactory) {
Expand Down Expand Up @@ -106,9 +109,6 @@ public FetchableFluentQuery<R> limit(int limit) {
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {

Assert.notNull(resultType, "Projection target type must not be null");
if (!resultType.isInterface()) {
throw new UnsupportedOperationException("Class-based DTOs are not yet supported.");
}

return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
scroll, countOperation, existsOperation, entityManager, projectionFactory);
Expand Down Expand Up @@ -155,7 +155,7 @@ public Window<R> scroll(ScrollPosition scrollPosition) {

Assert.notNull(scrollPosition, "ScrollPosition must not be null");

return scroll.scroll(sort, limit, scrollPosition).map(getConversionFunction());
return scroll.scroll(returnedType, sort, limit, scrollPosition).map(getConversionFunction());
}

@Override
Expand Down Expand Up @@ -183,7 +183,7 @@ public boolean exists() {

private TypedQuery<S> createSortedAndProjectedQuery() {

TypedQuery<S> query = finder.apply(sort);
TypedQuery<S> query = finder.apply(returnedType, sort);

if (!properties.isEmpty()) {
query.setHint(EntityGraphFactory.HINT, EntityGraphFactory.create(entityManager, entityType, properties));
Expand Down Expand Up @@ -227,16 +227,17 @@ private Function<Object, R> getConversionFunction() {

static class SpecificationScrollDelegate<T> extends ScrollDelegate<T> {

private final ScrollQueryFactory scrollFunction;
private final ScrollQueryFactory<TypedQuery<T>> scrollFunction;

SpecificationScrollDelegate(ScrollQueryFactory scrollQueryFactory, JpaEntityInformation<T, ?> entity) {
SpecificationScrollDelegate(ScrollQueryFactory<TypedQuery<T>> scrollQueryFactory,
JpaEntityInformation<T, ?> entity) {
super(entity);
this.scrollFunction = scrollQueryFactory;
}

public Window<T> scroll(Sort sort, int limit, ScrollPosition scrollPosition) {
public Window<T> scroll(ReturnedType returnedType, Sort sort, int limit, ScrollPosition scrollPosition) {

Query query = scrollFunction.createQuery(sort, scrollPosition);
Query query = scrollFunction.createQuery(returnedType, sort, scrollPosition);

if (limit > 0) {
query = query.setMaxResults(limit);
Expand Down
Loading

0 comments on commit 6fe1b2b

Please sign in to comment.