I have a tree structure that I need to convert to a DTO starting from root node. Currently I query the root node with JPA findById and I have to traverse the whole tree through a DFS to convert each node to a DTO, resulting in a lot of SQL selects, one for each time I call getChildren. It get even worse with the other associations that I also need to retrieve.
I would like to initialize the whole tree at once, with every associations also loaded, then do my DFS without worrying about triggering too much selects.
The node entity looks like this.
import jakarta.persistence.*;
import org.hibernate.annotations.BatchSize;
import java.util.LinkedHashSet;
import java.util.Set;
@Entity
public class TreeNode {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "treenode_seq")
@SequenceGenerator(name = "treenode_seq")
@Column(name = "id", nullable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private TreeNode parent;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@BatchSize(size = 100)
private Set<TreeNode> children = new LinkedHashSet<>();
@OneToMany(mappedBy = "treeNode", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@BatchSize(size = 100)
private Set<Association> associations = new LinkedHashSet<>();
// Getters and setters
}
As you can see it also has an oneToMany association, the association itself has another one to many association resulting in multiple n+1 selects while traversing the tree.
And this is the DTO
import java.util.List;
public class TreeNodeDTO {
private Long id;
private Long parentId;
private List<TreeNodeDTO> children;
private List<AssociationDTO> associations;
//getters and setters
}
I thinked about doing a CTE query with inner joins but when I select the root node, only direct children are initialized. It seems to make batching more efficient because all treeNode are in persistence context before I traverse the tree.
@Query(value = "WITH RECURSIVE TreeCTE AS (" +
" SELECT * FROM tree_node WHERE id = :parentId" +
" UNION ALL" +
" SELECT t.* FROM tree_node t JOIN TreeCTE c ON c.id = t.parent_id" +
")" +
"SELECT DISTINCT t.* FROM TreeCTE t where t.id = :parentId INNER JOIN tree_node tchildren on tchildren.parent_id = t.id", nativeQuery = true)
TreeNode findTreeWithChildren(@Param("parentId") Long parentId);
How can I initialize all my tree (including other associations in the object graph) before I traverse it with a DFS, so accessing unloaded entities and collections won't cause database roundtrips?