Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JPA Criteria query eager fetch associated entities using a SINGLE query with joins instead of multiple queries

We are moving from Hibernate native criteria to JPA criteria queries in scope of upgrading a hibernate from 4.3.11 to 5.2.12 and found out different behavior. Previously hibernate criteria use a single query with joins to eager fetch one-to-many associated entities, but JPA use separate queries to fetch the associated entities for each root entity.

I know I can explicitly set fetch mode like entityRoot.fetch("attributes", JoinType.INNER); but we need to do it in some AbstractDao implementation that should work for any eager one-to-many association so can't explicitly set this.

So can I somehow tell JPA criteria to eager fetch associated entities in a single query using joins by default instead of separate queries for each root entity?

The code example:

    CriteriaBuilder builder = createCriteriaBuilder();
    CriteriaQuery<T> criteriaQuery = builder.createQuery(getEntityClass());
    Root<T> entityRoot = criteriaQuery.from(getEntityClass());

    criteriaQuery.select(entityRoot);
    criteriaQuery.where(builder.equal(entityRoot.get("param1"), "value"));

    return getEntityManager().createQuery(criteriaQuery).getResultList();
like image 506
Yuriy Barannikov Avatar asked Nov 15 '17 13:11

Yuriy Barannikov


1 Answers

Short answer

You can't configure it in such a way, but you may implement the necessary behavior.

Long answer

As you may read in Hibernate 5.2 User Guide, there are several ways to apply a fetching strategy:

  • @Fetch annotation
  • JPQL/HQL query - fetch join
  • JPA Criteria query - FetchParent::fetch
  • JPA entity graph - attributeNodes
  • Hibernate profile - fetchOverrides

@Fetch annotation is a static way to apply fetching strategy, and the FetchMode.JOIN works exactly as you've described:

Inherently an EAGER style of fetching. The data to be fetched is obtained through the use of an SQL outer join.

The problem is, even if you would mark your attributes collection with the @Fetch(FetchMode.JOIN) annotation, it would be overridden:

The reason why we are not using a JPQL query to fetch multiple Department entities is because the FetchMode.JOIN strategy would be overridden by the query fetching directive.

To fetch multiple relationships with a JPQL query, the JOIN FETCH directive must be used instead.

Therefore, FetchMode.JOIN is useful for when entities are fetched directly, via their identifier or natural-id.

JPA Criteria query without FetchParent::fetch would do the same.

Since you need a universal solution for an abstract DAO, the possible way is to process all eager one-to-many associations with reflection:

    Arrays.stream(getEntityClass().getDeclaredFields())
            .filter(field ->
                    field.isAnnotationPresent(OneToMany.class))
            .filter(field ->
                    FetchType.EAGER == field.getAnnotation(OneToMany.class).fetch())
            .forEach(field ->
                    entityRoot.fetch(field.getName(), JoinType.INNER));

Of course, calling reflection for every query would be inefficient. You may obtain all loaded @Entity classes from Metamodel, process them, and store results for further use:

    Metamodel metamodel = getEntityManager().getMetamodel();
    List<Class> entityClasses = metamodel.getEntities().stream()
            .map(Type::getJavaType)
            .collect(Collectors.toList());

    Map<Class, List<String>> fetchingAssociations = entityClasses.stream()
            .collect(Collectors.toMap(
                    Function.identity(),
                    aClass -> Arrays.stream(aClass.getDeclaredFields())
                            .filter(field -> 
                                    field.isAnnotationPresent(OneToMany.class))
                            .filter(field -> 
                                    FetchType.EAGER == field.getAnnotation(OneToMany.class).fetch())
                            .map(Field::getName)
                            .collect(Collectors.toList())
            ));
like image 80
Anatoly Shamov Avatar answered Nov 05 '22 09:11

Anatoly Shamov