I have a class Plan
in which there is a list of Activity
. The Activity
class has a reference to a single Plan
. Hence there is a OneToMany relationship like this:
@Entity
public class Plan {
@OneToMany(mappedBy = "Plan")
private List<Activity> activities;
}
@Entity
public class Activity {
@ManyToOne
@JoinColumn(name= "PLAN_ID")
private Plan plan;
}
I need to convert them to DTOs to be sent to presentation layer. So I have an assembler class to simply convert domain objects to POJO.
public class PlanAssembler {
public static PlanDTO makeDTO(Plan p) {
PlanDTO result = new PlanDTO();
result.setProperty(p.getProperty);
...
for (Activity a: p.getActivity()) {
// Here I need to iterate over each activity to convert it to DTO
// But in ActivityAssembler, I also need PlanDTO
}
As you can see, in PlanAssembler
, I need to iterate over all activities and convert them to ActivityDTO
but the trouble is, in ActivityAssembler
I also need the PlanDTO
to construct the ActivityDTO
. It's gonna be an infinite loop. How can I sort this out?
Please help.
Let's assume we have the following post
and post_comment
tables, which form a one-to-many relationship via the post_id
Foreign Key column in the post_comment
table.
Considering we have a use case that only requires fetching the id
and title
columns from the post
table, as well as the id
and review
columns from the post_comment
tables, we could use the following JPQL query to fetch the required projection:
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
When running the projection query above, we get the following results:
| p.id | p.title | pc.id | pc.review |
|------|-----------------------------------|-------|---------------------------------------|
| 1 | High-Performance Java Persistence | 1 | Best book on JPA and Hibernate! |
| 1 | High-Performance Java Persistence | 2 | A must-read for every Java developer! |
| 2 | Hypersistence Optimizer | 3 | It's like pair programming with Vlad! |
However, we don't want to use a tabular-based ResultSet
or the default List<Object[]>
JPA or Hibernate query projection. We want to transform the aforementioned query result set to a List
of PostDTO
objects, each such object having a comments
collection containing all the associated PostCommentDTO
objects:
We can use a Hibernate ResultTransformer
, as illustrated by the following example:
List<PostDTO> postDTOs = entityManager.createQuery("""
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
""")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();
assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());
The PostDTOResultTransformer
is going to define the mapping between the Object[]
projection and the PostDTO
object containing the PostCommentDTO
child DTO objects:
public class PostDTOResultTransformer
implements ResultTransformer {
private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();
@Override
public Object transformTuple(
Object[] tuple,
String[] aliases) {
Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);
Long postId = longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]);
PostDTO postDTO = postDTOMap.computeIfAbsent(
postId,
id -> new PostDTO(tuple, aliasToIndexMap)
);
postDTO.getComments().add(
new PostCommentDTO(tuple, aliasToIndexMap)
);
return postDTO;
}
@Override
public List transformList(List collection) {
return new ArrayList<>(postDTOMap.values());
}
}
The aliasToIndexMap
is just a small utility that allows us to build a Map
structure that associates the column aliases and the index where the column value is located in the Object[]
tuple
array:
public Map<String, Integer> aliasToIndexMap(
String[] aliases) {
Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>();
for (int i = 0; i < aliases.length; i++) {
aliasToIndexMap.put(aliases[i], i);
}
return aliasToIndexMap;
}
The postDTOMap
is where we are going to store all PostDTO
entities that, in the end, will be returned by the query execution. The reason we are using the postDTOMap
is that the parent rows are duplicated in the SQL query result set for each child record.
The computeIfAbsent
method allows us to create a PostDTO
object only if there is no existing PostDTO
reference already stored in the postDTOMap
.
The PostDTO
class has a constructor that can set the id
and title
properties using the dedicated column aliases:
public class PostDTO {
public static final String ID_ALIAS = "p_id";
public static final String TITLE_ALIAS = "p_title";
private Long id;
private String title;
private List<PostCommentDTO> comments = new ArrayList<>();
public PostDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]);
}
//Getters and setters omitted for brevity
}
The PostCommentDTO
is built in a similar fashion:
public class PostCommentDTO {
public static final String ID_ALIAS = "pc_id";
public static final String REVIEW_ALIAS = "pc_review";
private Long id;
private String review;
public PostCommentDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.review = stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]);
}
//Getters and setters omitted for brevity
}
That's it!
Using the PostDTOResultTransformer
, the SQL result set can be transformed into a hierarchical DTO projection, which is much convenient to work with, especially if it needs to be marshalled as a JSON response:
postDTOs = {ArrayList}, size = 2
0 = {PostDTO}
id = 1L
title = "High-Performance Java Persistence"
comments = {ArrayList}, size = 2
0 = {PostCommentDTO}
id = 1L
review = "Best book on JPA and Hibernate!"
1 = {PostCommentDTO}
id = 2L
review = "A must read for every Java developer!"
1 = {PostDTO}
id = 2L
title = "Hypersistence Optimizer"
comments = {ArrayList}, size = 1
0 = {PostCommentDTO}
id = 3L
review = "It's like pair programming with Vlad!"
It won't be an infinite loop because you have to use the PlanDTO object result which you have just created before the loop. See the code below.
Note : Still I suggest to go for a framework which will do this stuff for you.
public class PlanAssembler {
public static PlanDTO makeDTO(Plan p) {
PlanDTO result = new PlanDTO();
result.setProperty(p.getProperty);
...
for (Activity a: p.getActivity()) {
ActivityDTO activityDTO = new ActivityDTO();
// Here I need to iterate over each activity to convert it to DTO
// But in ActivityAssembler, I also need PlanDTO
//Code to convert Activity to ActivityDTO.
activityDTO.setPlan(result);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With