Cosium / spring-data-jpa-entity-graph

Spring Data JPA extension allowing full dynamic usage of EntityGraph on repositories
MIT License
478 stars 49 forks source link

Support for (Nested) Projections #70

Closed reda-alaoui closed 2 years ago

reda-alaoui commented 2 years ago

Discussed in https://github.com/Cosium/spring-data-jpa-entity-graph/discussions/69

Originally posted by **dohorn** July 15, 2022 First of all, I think this project is a super useful and necessary addition to Spring Data JPA 👍 What I would like to accomplish right now is to use this for nested projections on lazily loaded 1-To-Many Relations. Doing this without any entity graph usually ends in a LazyInitializationException. However annotating the projection method with an entity graph solves this issue, like so: ```Java public interface ProductDto { String getName(); List getRatings(); } public interface RatingDto { int getRating(); } public interface BaseProductRepository extends Repository, EntityGraphQuerydslPredicateExecutor { @EntityGraph(attributePaths = {"ratings"}) Optional findById(String id); } ``` As I want to do this in a dynamic way, this project would suit my purposes perfectly, if you decided to add support.
reda-alaoui commented 2 years ago

@dohorn Thinking more about it, I doubt Spring Data takes into account the EntityGraph passed by the annotation. To my knowledge, Spring Data performs a multiselect to perform the query in the case of a projection. I think that Spring data just ignores the EntityGraph that you provide via the annotation in your example:

public interface ProductDto {
    String getName();
    List<RatingDto> getRatings();
}

public interface RatingDto {
    int getRating();
}

public interface BaseProductRepository<T extends AbstractProduct> extends Repository<T, String>, EntityGraphQuerydslPredicateExecutor<T> {
    @EntityGraph(attributePaths = {"ratings"})
    Optional<ProductDto> findById(String id);
}

Can you check that please?

reda-alaoui commented 2 years ago

@dohorn I am closing this for now. If you think I am missing something, tell me.

SimoneFalzone commented 1 year ago

Hello @reda-alaoui,

I've been exploring your project recently, and I must say I am interested on this feature.

I steped a little bit through the code an found this lines of code, in the method com.cosium.spring.data.jpa.entity.graph.repository.support.EntityGraphQueryHintCandidates#canApplyEntityGraph.

private boolean canApplyEntityGraph(ResolvableType repositoryMethodReturnType) {

    Class<?> resolvedReturnType = repositoryMethodReturnType.resolve();
    if (resolvedReturnType != null
        && (Void.TYPE.equals(resolvedReturnType)
            || domainClass.isAssignableFrom(resolvedReturnType))) {
      return true;
    }
    for (Class<?> genericType : repositoryMethodReturnType.resolveGenerics()) {
      if (domainClass.isAssignableFrom(genericType)) {
        return true;
      }
    }
    return false;
  }

I am not sure what the purpose is, but here if a DTO Class is the repositoryMethodReturnType, the method always returns false. If the method would return true on Dto Projections, Spring would parse it correctly in org.springframework.data.repository.query.ResultProcessor#processResult(java.lang.Object, org.springframework.core.convert.converter.Converter<java.lang.Object,java.lang.Object>)

public <T> T processResult(@Nullable 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");

        ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter);

        if (source instanceof Window<?> && method.isScrollQuery()) {
            return (T) ((Window<?>) source).map(converter::convert);
        }

        if (source instanceof Slice && (method.isPageQuery() || method.isSliceQuery())) {
            return (T) ((Slice<?>) source).map(converter::convert);
        }

        if (source instanceof Collection<?> collection && method.isCollectionQuery()) {

            Collection<Object> target = createCollectionFor(collection);

            for (Object columns : collection) {
                target.add(type.isInstance(columns) ? columns : converter.convert(columns));
            }

            return (T) target;
        }

        if (source instanceof Stream && method.isStreamQuery()) {
            return (T) ((Stream<Object>) source).map(t -> type.isInstance(t) ? t : converter.convert(t));
        }

        if (ReactiveWrapperConverters.supports(source.getClass())) {
            return (T) ReactiveWrapperConverters.map(source, it -> processResult(it, preparingConverter));
        }

        return (T) converter.convert(source);
    }

And Spring does incount EntityGraphs annotations to projections, it than buidls a join query.

I look forward to hearing back from you

Eng-Fouad commented 1 year ago

Closed Projection does not work with @EntityGraph