0

Is there a way to use @NamedEntityGraph for eager fetch of multiple collections of entities? I'm using Spring boot with Spring data and hibernate.

I have 2 entities- grandfather and father who have OneToMany relation. I need to get all of the grandfather fathers and then use a function on each of them (for simplicity let's say .toString()) The problem is that the Father entity has 25 different type of children and each one is a collection with FetchType.LAZY and relations of ManyToMany, so calling toString loades all of the children collections and make the function very slow. The entities looks like that:

public class GrandFather() {
  private Integer id;
  @OneToMany(fetch = FetchType.LAZY)
  private Set<Father> fathers;
}

public class Father() {
  private Integer id;
  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(
    name = "father_child_type_one"
    joinColumns = @JoinColumn(name = "fk_father_id_child_type_one", referencedColumnName = "id"),
   inverseJoinColumns = @JoinColumn(name = "fk_father_child_type_one_id", referencedColumnName = "id")
  )
  private Set<Child1> typeOneChildren;

.
.
.

@ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(
    name = "father_child_type_twenty_five"
    joinColumns = @JoinColumn(name = "fk_father_id_child_type_twenty_five", referencedColumnName = "id"),
   inverseJoinColumns = @JoinColumn(name = "fk_father_child_type_twenty_five_id", referencedColumnName = "id")
  )
  private Set<Child25> typeTwentyFiveChildren;

  public void toString() {
    typeOneChildren.stream().forEach(child -> child.toString());
    .
    .
    .
    typeTwentyFiveChildren.stream().forEach(child -> child.toString());
  }
}

Each one of the children are a different Entity with different properties.

I tried adding @NamedEntityGraph to GrandFather like this:

@NamedEntityGraph(name = "fetchGrandFatherWithChildren",
        attributeNodes = {
                @NamedAttributeNode(value = "fathers", subgraph = "father_subgraph")
        },
        subgraphs = {
                @NamedSubgraph(name = "father_subgraph", type = Father.class, attributeNodes = {
                        @NamedAttributeNode("typeOneChildren"),
                        .
                        .
                        .
                        @NamedAttributeNode("typeTwentyFiveChildren")
                })
        })

but this EntityGraph only worked when I put it on findById (most of the app uses this function and doesn't need the graph) in GrandFatherRepository, every other name and it didn't work - either just kept on doing all the lazy fetching for each father on each child type or throwing MultipleBagFetchException (which make sense with all of the collections and Cartesian results).

public class GrandFatherService {
    GrandFatherRepository grandFatherRepository;
    public void toString(Integer grandfatherId) {
      Set<Father> fathers = grandFatherRepository.findById(grandfatherId);
      fathers.stream().forEach(Father::toString);
    }
}

The other solution I found was based on the answer for this qestion and it as follows (it works but very messy): in my GrandFatherService in the function that I try to fetch the GrandFather with all of his grandchildren eagerly, I get the GrandFather with the fathers eagerly, and then I fetch each type of child for all of the fathers and set them. (findWithFathersById uses a basic entity graph)

public class GrandFatherService {
  GrandFatherRepository grandFatherRepository;
  FatherRepository fatherRepository;
  public void toString(Integer grandfatherId) {
    Set<Father> fathers = grandFatherRepository.findWithFathersById(grandfatherId);

    Map<Interer, Father> fatherChildOne = fatherRepository.findAllWithChildTypeOne(grandfatherId);
    .
    .
    .
    Map<Interer, Father> fatherChildTwentyFive = fatherRepository.findAllWithChildTypeTwentyFive(grandfatherId);

   fathers.stream().forEach(father -> {
     Integer fatherId = father.getId();
     father.setChildTypeOne(fatherChildOne.get(fatherId).getChildOne);
     .
     .
     .
     father.setChildTypeTwentyFive(fatherChildTwentyFive.get(fatherId).getChildTwentyFive);
     father.toString();
   }
  }
}

public interface FatherRepository extends JpaRepository<Father, Integer> {
  @Query(value = "select f from Father f LEFT JOIN FETCH f.typeOneChildren")
  Set<Father> findAllWithChildTypeOne(Integer trialId);
  .
  .
  .
  @Query(value = "select f from Father f LEFT JOIN FETCH f.typeTwentyFiveChildren")
  Set<Father> findAllWithChildTypeTwentyFive(Integer trialId);
}

For conclusion, my question is: Is there a less messy way to eager fetch all of the grand children/ to make the first entity graph to work not with findById?

hido
  • 63
  • 8
  • Have you tried using a batchsize annotation on the collection mappings? I don’t know if hibernate will respect it with a fetch graph, but will if the collection is lazily accessed. It will force one query to return multiple collection results, so greatly reduces the number of queries involved – Chris Jan 16 '23 at 03:28
  • even if my batch size is 100 and I only have 1 instance of each child type, it will still do 25 queries for each father to get each child type – hido Jan 16 '23 at 08:20
  • Pick your poison. 25 queries, or 1 query (if it would work) that returned the same father row data N^25 times, where N is the number of children in the average collection. At least with batch queries, you are going to get a fixed number of queries (26) instead of 1+N*25 for N parents returned. Not using fetch joins across ToMany relationships means you can use pagination as well. Other providers have other options around batch reading https://www.eclipse.org/eclipselink/documentation/2.7/solutions/performance001.htm#CHDFHFEB – Chris Jan 16 '23 at 16:19
  • even a fixed number of 25 queries per entity is too much for me because I might have 2000 fathers and that 50k queries for children if it is lazy – hido Jan 17 '23 at 07:41
  • With batch reading, there will only be 1+ 25 queries, no matter how many fathers (depending on the batch size). batching is different in that while it is a separate query from the one to bring in fathers, it brings in the children to populate the collections for multiple fathers. See the solution in EclipseLink as they have more options and IMO better descriptions of the problem it solves. It greatly reduces the number of queries involved, while not suffering from the cartesian join issue if you were to join all those relationships into one query. – Chris Jan 17 '23 at 15:46

1 Answers1

0

Like Chris mentioned in the comments, there is always a tradeoff with joins vs batch queries. I think this is a perfect use case for Blaze-Persistence Entity Views though since it offers the MULTISET fetch strategy that represents the best of both worlds.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(GrandFather.class)
public interface GrandFatherDto {
    @IdMapping
    Integer getId();
    String getName();
    Set<FatherDto> getFathers();

    @EntityView(Father.class)
    interface FatherDto {
        @IdMapping
        Long getId();
        String getName();
        @Mapping(fetch = MULTISET)
        Set<Child1Dto> getTypeOneChildren();
        //...
        @Mapping(fetch = MULTISET)
        Set<Child25Dto> getTypeTwentyFiveChildren();
    }
    @EntityView(Child1.class)
    interface Child1Dto {
        @IdMapping
        Long getId();
        String getName();
    }

    //...

    @EntityView(Child25.class)
    interface Child25Dto {
        @IdMapping
        Long getId();
        String getName();
    }
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

GrandFatherDto a = entityViewManager.find(entityManager, GrandFatherDto.class, id);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<GrandFatherDto> findAll(Pageable pageable);

The best part is, it will only fetch the state that is actually necessary!

Christian Beikov
  • 15,141
  • 2
  • 32
  • 58