Search code examples
hibernatemutinyhibernate-reactive

Hibernate Reactive - Best practice for fetching child associations during iterating parent entities


consider the following sample. I have two entities: Author and Book. Their signatures are:

@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "author")
public class Author implements Serializable {

    @Serial
    private static final long serialVersionUID = 7626370553439538790L;

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @Default
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "author", cascade = CascadeType.ALL)
    private Set<Book> books = new HashSet<>();
}

and

@Getter
@Setter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "book")
public class Book implements Serializable {

    @Serial
    private static final long serialVersionUID = 4454993533777924839L;

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;
}

I want to query Author and produce List<AuthorResponse>. The AuthorResponse contains attributes similar to the Author and Set<BookResponse>, whereas the BookResponse has attributes identical to the Book. Hence I have written the following code:

public Uni<List<AuthorResponse>> getAuthors() {
    // @formatter:off
    return sessionFactory.withSession(
        (Session session) -> {
            CriteriaBuilder criteriaBuilder = sessionFactory.getCriteriaBuilder();
            CriteriaQuery<Author> criteriaQuery = criteriaBuilder.createQuery(Author.class);
            Root<Author> authorTable = criteriaQuery.from(Author.class);
            criteriaQuery.select(authorTable);
            Query<Author> query = session.createQuery(criteriaQuery);
            query.setFirstResult(0);
            query.setMaxResults(10);
            return query.getResultList()
                .onItem()
                .transform(
                    (List<Author> authors) -> authors
                        .stream()
                        .map(
                            (Author author) -> AuthorResponse.builder()
                                .id(author.getId())
                                .name(author.getName())
                                .books(
                                    author.getBooks()
                                        .stream()
                                        .map(
                                            (Book book) -> BookResponse.builder()
                                                .id(book.getId())
                                                .name(book.getName())
                                                .build()
                                        )
                                        .collect(Collectors.toSet())
                                )
                                .build()
                        )
                        .collect(Collectors.toList())
                );
        }
    );
    // @formatter:on
}

The code author.getBooks() clearly throws LazyInitializationException, unless I don't initialize it explicitly either with session.fetch() or Mutiny.fetch(). The problem is that invoking either of these two methods within the above code chain doesn't fit, because it returns Uni<Set<Book>>, unless I do the following:

Mutiny.fetch(author.getBooks())
    .onItem()
    .transform(
        (Set<Book> books) -> books.stream()
            .map(
                (Book book) -> BookResponse.builder()
                    .id(book.getId())
                    .name(book.getName())
                    .build()
            )
            .collect(Collectors.toSet()))
            .await()
            .indefinitely()

and clearly, it is the anti-patten of reactiveness (if my understanding is correct).

So to mitigate the above situation I have used EntityGraph as follows:

EntityGraph<Author> entityGraph = session.createEntityGraph(Author.class);
entityGraph.addAttributeNodes("book");
Query<Author> query = session.createQuery(criteriaQuery);
query.setPlan(entityGraph);

Afterward, it is working.

I am wondering if using the EntityGraph in such situations is a good practice or not. Or is there any better way?

Any suggestions would be appreciated.

Regards, Tapas


Solution

  • I've already answered on GitHub, but I guess I will repeat it here.

    Usually, fetching the result using an eager fetch in a JPQL query or an EntityGraph is a better approach, because you will load the association with a single query. It makes sense if you know that you will need the associated elements.

    But you can still use Mutiny.fetch without blocking. I would convert the Uni<List<Author>> into a Multi<Author>:

         return query.getResultList()
            // Convert the Uni<List<Author> into a Multi<Author>
            .onItem().transformToMulti( Multi.createFrom()::iterable )
    
            // For each author fetch the books
            .onItem().call( author -> Mutiny.fetch( author.getBooks() ) )
    
            // Now everything has been fetched and you can build the response
            .map( this::buildAuthorResponse )
    
            // Convert the Multi<AuthorResponse> into Uni<List<AuthorResponse>>
            .collect().asList();
    
    ...
    
    
    private void AuthorResponse buildAuthorResponse(Author author) {
       return AuthorResponse.builder()
                                    .id(author.getId())
                                    .name(author.getName())
                                    .books(
                                        author.getBooks()
                                            .stream()
                                            .map(
                                                (Book book) -> BookResponse.builder()
                                                    .id(book.getId())
                                                    .name(book.getName())
                                                    .build()
                                            )
                                            .collect(Collectors.toSet())
                                    )
                                    .build();
    }
    

    Note that if you are using Quarkus, it might not be necessary to collect the results into a Uni<List<AuthorResponse>> and you could just return Multi<AuthorReponse>.

    Anywyay, all approaches are valid and you can pick the one that better suit your use case. Just keep in mind that fetching the association for each result in the list will cause a new query each time and it's usually not recommended (N+1 problem).