Issue
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).
Answered By - Davide D'Alto
Answer Checked By - Cary Denson (JavaFixing Admin)