Search code examples
spring-bootspring-data-jpaentitygraph

Incomplete OneToMany collections when using JPA findOne with EntityGraph


During the migration from Spring-Boot:2.7.4 to Spring-Boot:3.0.9, I notice that EntityGraphJpaSpecificationExecutor.findOne(Specification<T> spec, EntityGraph entityGraph) does not completely resolve OneToMany collection.

With spring-boot:2.7.4 this worked very well. I can't find any information about this problem in the migration instructions.

This can be seen in the small demo. When calling findOne(with(authorId), entityGraph) not all books are resolved for the author - test will fail.

I expect all lazy fetched OneToMany details to be resolved.

You can find small demo here: https://github.com/da-von/spring-boot-jpa-findOne

When logging the generated SQL queries, it is visible that left join is combined with fetch first ? rows only limitation. This seems to make no sense to me, as EntityGraphs can be dynamic and the results must also be completely resolved for dependent details.

select a1_0.id,b1_0.author_id,b1_0.id,b1_0.genre,b1_0.isbn,b1_0.title,a1_0.name 
  from author a1_0 
      left join book b1_0 on a1_0.id=b1_0.author_id 
  where a1_0.id=? 
  fetch first ? rows only
  • Local Machine: MacBook Pro, macOS Ventura:13.4.1
  • java:17 locally existing VM temurin-17.jdk
  • spring-boot:3.0.9
  • com.cosium.spring.data:spring-data-jpa-entity-graph:3.0.1
  • Postgres in Docker image Postgres:13-alpine

Solution

  • Solved! Override repository interface with:

    @NoRepositoryBean
    public interface OverrideEntityGraphSimpleJpaRepository<T, ID> extends JpaRepository<T, ID>, EntityGraphPagingAndSortingRepository<T, ID>, EntityGraphJpaSpecificationExecutor<T> {
    
        @Override
        default Optional<T> findOne(Specification<T> spec) {
            return findOne(spec, (EntityGraph) null);
        }
    
        @Override
        default Optional<T> findOne(Specification<T> spec, EntityGraph entityGraph) {
            val items = findAll(spec, entityGraph);
    
            if (items.size() > 1) {
                throw new IncorrectResultSizeDataAccessException(1);
            }
            if (items.size() == 1) {
                return Optional.of(items.get(0));
            }
            return Optional.empty();
        }
    }
    
    

    AuthorRepository and BookRepository extend this interface, executing the overridden findOne(...) variants:

    public interface AuthorRepository extends OverrideEntityGraphSimpleJpaRepository<Author, Long>,
            EntityGraphJpaSpecificationExecutor<Author> {
    
        default Optional<Author> findOne(long id, EntityGraph entityGraph) {
            return findOne(
                      (a, cb, cq) -> cq.equal(a.get(Author_.id), id), 
                       entityGraph
            );
        } 
    
        // other detail ommitted
    
    }
    

    Solution is committed to the sample repository https://github.com/Cosium/spring-data-jpa-entity-graph