29

(Using Spring Data JPA) I have two entities Parent& Child with a OneToMany/ManyToOne bi-directional relationship between them. I add a @NamedEntityGraph to the parent entity like so:

@Entity
@NamedEntityGraph(name = "Parent.Offspring", attributeNodes = @NamedAttributeNodes("children"))
public class Parent{
//blah blah blah

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
Set<Child> children;

//blah blah blah
}

Notice that the fetch type for the Parent's children is LAZY. This is on purpose. I don't always want to eager load the children when I'm querying an individual parent. Normally I could use my named entity graph to eager load the children on-demand, so to speak. But.....

There is a specific situation where I'd like to query for one or more parents AND eager load their children. In addition to this I need to be able to build this query programmatically. Spring Data provides the JpaSpecificationExecutor which allows one to build dynamic queries, but I can't figure out how to use it in conjunction with entity graphs for eager loading children in this specific case. Is this even possible? Is there some other way to eager load 'toMany entities using specifications?

Kerby
  • 937
  • 2
  • 8
  • 16

7 Answers7

23

The solution is to create a custom repository interface that implements these features:

@NoRepositoryBean
public interface CustomRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    List<T> findAll(Specification<T> spec, EntityGraphType entityGraphType, String entityGraphName);
    Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraphType entityGraphType, String entityGraphName);
    List<T> findAll(Specification<T> spec, Sort sort, EntityGraphType entityGraphType, String entityGraphName);
    T findOne(Specification<T> spec, EntityGraphType entityGraphType, String entityGraphName);

}

Also create an implementation:

@NoRepositoryBean
public class CustomRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CustomRepository<T, ID> {

    private EntityManager em;

    public CustomRepositoryImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.em = em;
    }

    @Override
    public List<T> findAll(Specification<T> spec, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, (Sort) null);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getResultList();
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, pageable.getSort());
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return readPage(query, pageable, spec);
    }

    @Override
    public List<T> findAll(Specification<T> spec, Sort sort, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, sort);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getResultList();
    }

    @Override
    public T findOne(Specification<T> spec, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
        TypedQuery<T> query = getQuery(spec, (Sort) null);
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
        return query.getSingleResult();
    }
}

And create a factory:

public class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new CustomRepositoryFactory(entityManager);
    }

    private static class CustomRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {

        private EntityManager entityManager;

        public CustomRepositoryFactory(EntityManager entityManager) {
            super(entityManager);
            this.entityManager = entityManager;
        }

        protected Object getTargetRepository(RepositoryMetadata metadata) {
            return new CustomRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), entityManager);
        }

        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            // The RepositoryMetadata can be safely ignored, it is used by the JpaRepositoryFactory
            //to check for QueryDslJpaRepository's which is out of scope.
            return CustomRepository.class;
        }
    }

}

And change the default repository factory bean to the new bean, e.g. in spring boot add this to the configuration:

@EnableJpaRepositories(
    basePackages = {"your.package"},
    repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class
)

For more info about custom repositories: http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.custom-behaviour-for-all-repositories

Ortomala Lokni
  • 56,620
  • 24
  • 188
  • 240
Joep
  • 4,003
  • 3
  • 28
  • 32
17

I managed to implement this with overriding of findAll method and adding to it annotation @EntityGraph:

public interface BookRepository extends JpaSpecificationExecutor<Book> {
   @Override
   @EntityGraph(attributePaths = {"book.author"})
   List<Book> findAll(Specification<Book> spec);
}
vteraz
  • 181
  • 1
  • 4
  • 2
    Very elegant solution. Thanks :-) – John Smith Nov 13 '20 at 14:57
  • @vteraz I can't get this to work on the overridden `findById` of JpaRepository using a NamedEntityGraph declared on the Entity. What's the difference to the example above? Declaring a new method `findWithAttributesAll` will respect the NamedEntityGraph. – leo Dec 12 '20 at 10:41
  • it works for me even with the `@NamedEntityGraph`. Can you post an example of your case? – vteraz Dec 14 '20 at 13:30
  • Is there a way to use this solution for loading multiple OneToMany associations? – Raj Jul 24 '21 at 12:40
9

The Joepie response is O.K.

but you don't need to create repositoryFactoryBeanClass, set up repositoryBaseClass

@EnableJpaRepositories(
    basePackages = {"your.package"},
    repositoryBaseClass = CustomRepositoryImpl.class)
pbo
  • 101
  • 1
  • 2
9

Project Spring Data JPA EntityGraph implements some of the approaches mentioned in the other answers.

It has for example these additional repository interfaces:

  • EntityGraphJpaRepository which is equivalent to standard JpaRepository
  • EntityGraphJpaSpecificationExecutor which is equivalent to standard JpaSpecificationExecutor

Check the reference documentation for some examples.

Tomas Pinos
  • 2,812
  • 14
  • 22
3

To complement the answers of Joep and pbo, I have to say that with new versions of Spring Data JPA, you will have to modify the constructor of CustomRepositoryImpl. Now the documentation says:

The class needs to have a constructor of the super class which the store-specific repository factory implementation is using. In case the repository base class has multiple constructors, override the one taking an EntityInformation plus a store specific infrastructure object (e.g. an EntityManager or a template class).

I use the following constructor:

public CustomRepositoryImpl(JpaEntityInformation<T,?> entityInformation, EntityManager em) {
    super(entityInformation, em);
    this.domainClass = entityInformation.getJavaType();
    this.em = em;
}

I've also added a private field to store the domain class:

private final Class<T> domainClass;

This allow me to get rid of the deprecated method readPage(javax.persistence.TypedQuery<T> query, Pageable pageable, @Nullable Specification<T> spec) and use instead:

@Override
public Page<T> findAll(Specification<T> spec, Pageable pageable, EntityGraph.EntityGraphType entityGraphType, String entityGraphName) {
    TypedQuery<T> query = getQuery(spec, pageable.getSort());
    query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
    return readPage(query, domainClass, pageable, spec);
 }
Ortomala Lokni
  • 56,620
  • 24
  • 188
  • 240
  • 1
    You could simply use the SimpleJpaRepository `getDomainClass()` method instead (https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/support/SimpleJpaRepository.html#getDomainClass--) – Kruschenstein Jul 15 '20 at 08:24
0

Factory of repository bean is not necessary by the way. Do you need just a simple Bean.

You can see my full examples here: https://github.com/netstart/POCs/tree/master/jpa/jpaspecification-whith-entity-graph


import com.specgraph.entitygraph.model.Characteristic;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

@NoRepositoryBean
public class CharacteristicsJpaSpecificationRepository extends SimpleJpaRepository<Characteristic, Long> {

   private final EntityManager em;

   public CharacteristicsJpaSpecificationRepository(Class<Characteristic> domainClass, EntityManager em) {
       super(domainClass, em);
       this.em = em;
   }

   public Page<Characteristic> findByTypeUsingSpecification(String type, Pageable pageable) {
       Specification<Characteristic> spec =
           (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("type"), type);

       return findAll(spec, pageable, EntityGraphType.FETCH, "Characteristic.item");
   }

   public Page<Characteristic> findAll(Specification<Characteristic> spec,
                                       Pageable pageable,
                                       EntityGraph.EntityGraphType entityGraphType,
                                       String entityGraphName) {

       TypedQuery<Characteristic> query = getQuery(spec, pageable.getSort());
       query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraphName));
       return readPage(query, Characteristic.class, pageable, spec);
   }

}

import com.specgraph.entitygraph.model.Characteristic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class MyRepositoryConfiguration {

    @PersistenceContext // this will inject em in your class
    private EntityManager em;

    @Bean
    public CharacteristicsJpaSpecificationRepository getCharacteristicsJpaSpecificationRepository() {
        return new CharacteristicsJpaSpecificationRepository(Characteristic.class, em);
    }

}

import com.specgraph.entitygraph.model.Characteristic;
import com.specgraph.entitygraph.repository.specentitygraph.CharacteristicsJpaSpecificationRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.jdbc.Sql;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@Sql(scripts = "/entitygraph-data.sql")
public class CharacteristicsJpaSpecificationTest2 {

    @Autowired
    private CharacteristicsJpaSpecificationRepository characteristicsJpaSpecificationRepository;

    @Test
    public void find() {
        int pageNumber = 1;
        int pageSize = 10;
        PageRequest pageable = PageRequest.of(pageNumber, pageSize);

        Page<Characteristic> page =
            characteristicsJpaSpecificationRepository.findByTypeUsingSpecification("Rigid", pageable);

        assertThat(page.getTotalElements()).isEqualTo(1);
    }

}
Clayton K. N. Passos
  • 1,292
  • 12
  • 15
0

Take a look at Composite Specification API, it does what you need. This class implements the Specification interface and may be composed as in the example. In your case to fetch parents with surname "Doe" and their children born before 1.1.1990, you will need the specifications:

public final class ParentSpecifications {

    private ParentSpecifications() {
    }

    public static <S extends Path<Parent>> CompositeSpecification<Parent, S> surname(String surname) {
        return CompositeSpecification.<Parent, S, TypeSafePredicateBuilder<Path<Parent>>>of(
                (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("surname"), surname)
        );
    }

    public static <S extends From<?, Parent>> CompositeSpecification<Parent, S> fetchChildren(CompositeSpecification<?, ? super Join<?, Child>> childSpecification) {
        return CompositeSpecification.<Parent, S, TypeSafePredicateBuilder<From<?, Parent>>>of(
                (root, query, criteriaBuilder) -> {
                    query.distinct(true);
                    return childSpecification.asBuilder().toPredicate((Join<Parent, Child>) root.<Parent, Child>fetch("children", JoinType.LEFT), query, criteriaBuilder);
                });
    }
}

public final class ChildSpecifications {

    private ChildSpecifications() {
    }
    
    public static <S extends Path<Child>> CompositeSpecification<Child, S> dateOfBirth(CompositeSpecification<?, ? super Path<LocalDate>> dateOfBirthSpecification) {
        return CompositeSpecification.<Child, S, TypeSafePredicateBuilder<Path<Child>>>of(
                (root, query, criteriaBuilder) -> dateOfBirthSpecification.asBuilder().toPredicate(root.get("dateOfBirth"), query, criteriaBuilder)
        );
    }
}

and the query:

var parents = parentRepository.findAll(surname("Doe").and(fetchChildren(dateOfBirth(lessThan(LocalDate.of(1990,1,1))))));
Bartosz Popiela
  • 951
  • 10
  • 14