feat(#248): enrich TagTreeNodeDTO with parentId and populate documentCount via single aggregate query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,4 +3,4 @@ package org.raddatz.familienarchiv.dto;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record TagTreeNodeDTO(UUID id, String name, String color, int documentCount, List<TagTreeNodeDTO> children) {}
|
public record TagTreeNodeDTO(UUID id, String name, String color, int documentCount, List<TagTreeNodeDTO> children, UUID parentId) {}
|
||||||
|
|||||||
@@ -141,12 +141,17 @@ public class TagService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all tags assembled into a tree. Document counts are not included here
|
* Returns all tags assembled into a tree with document counts per node.
|
||||||
* (they are populated by the controller layer if needed, or set to 0).
|
* Uses a single aggregate query to avoid N+1 behaviour.
|
||||||
*/
|
*/
|
||||||
public List<TagTreeNodeDTO> getTagTree() {
|
public List<TagTreeNodeDTO> getTagTree() {
|
||||||
List<Tag> all = tagRepository.findAll();
|
List<Tag> all = tagRepository.findAll();
|
||||||
return buildTree(all);
|
Map<UUID, Long> counts = tagRepository.findDocumentCountsPerTag().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (UUID) row[0],
|
||||||
|
row -> (Long) row[1]
|
||||||
|
));
|
||||||
|
return buildTree(all, counts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── private helpers ─────────────────────────────────────────────────────
|
// ─── private helpers ─────────────────────────────────────────────────────
|
||||||
@@ -195,7 +200,7 @@ public class TagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TagTreeNodeDTO> buildTree(List<Tag> tags) {
|
private List<TagTreeNodeDTO> buildTree(List<Tag> tags, Map<UUID, Long> counts) {
|
||||||
Map<UUID, List<TagTreeNodeDTO>> childrenByParent = new HashMap<>();
|
Map<UUID, List<TagTreeNodeDTO>> childrenByParent = new HashMap<>();
|
||||||
|
|
||||||
for (Tag tag : tags) {
|
for (Tag tag : tags) {
|
||||||
@@ -206,14 +211,14 @@ public class TagService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Tag tag : tags) {
|
for (Tag tag : tags) {
|
||||||
|
int documentCount = counts.getOrDefault(tag.getId(), 0L).intValue();
|
||||||
TagTreeNodeDTO node = new TagTreeNodeDTO(
|
TagTreeNodeDTO node = new TagTreeNodeDTO(
|
||||||
tag.getId(), tag.getName(), tag.getColor(), 0,
|
tag.getId(), tag.getName(), tag.getColor(), documentCount,
|
||||||
childrenByParent.getOrDefault(tag.getId(), new ArrayList<>())
|
childrenByParent.getOrDefault(tag.getId(), new ArrayList<>()),
|
||||||
|
tag.getParentId()
|
||||||
);
|
);
|
||||||
if (tag.getParentId() != null) {
|
if (tag.getParentId() != null) {
|
||||||
childrenByParent.get(tag.getParentId()).add(node);
|
childrenByParent.get(tag.getParentId()).add(node);
|
||||||
} else {
|
|
||||||
childrenByParent.get(tag.getId()); // ensure root is tracked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,9 +226,11 @@ public class TagService {
|
|||||||
List<TagTreeNodeDTO> roots = new ArrayList<>();
|
List<TagTreeNodeDTO> roots = new ArrayList<>();
|
||||||
for (Tag tag : tags) {
|
for (Tag tag : tags) {
|
||||||
if (tag.getParentId() == null) {
|
if (tag.getParentId() == null) {
|
||||||
|
int documentCount = counts.getOrDefault(tag.getId(), 0L).intValue();
|
||||||
roots.add(new TagTreeNodeDTO(
|
roots.add(new TagTreeNodeDTO(
|
||||||
tag.getId(), tag.getName(), tag.getColor(), 0,
|
tag.getId(), tag.getName(), tag.getColor(), documentCount,
|
||||||
childrenByParent.getOrDefault(tag.getId(), new ArrayList<>())
|
childrenByParent.getOrDefault(tag.getId(), new ArrayList<>()),
|
||||||
|
null
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class TagControllerTest {
|
|||||||
void getTagTree_returns200_withTreeStructure() throws Exception {
|
void getTagTree_returns200_withTreeStructure() throws Exception {
|
||||||
UUID parentId = UUID.randomUUID();
|
UUID parentId = UUID.randomUUID();
|
||||||
UUID childId = UUID.randomUUID();
|
UUID childId = UUID.randomUUID();
|
||||||
TagTreeNodeDTO child = new TagTreeNodeDTO(childId, "Haus", null, 0, List.of());
|
TagTreeNodeDTO child = new TagTreeNodeDTO(childId, "Haus", null, 0, List.of(), parentId);
|
||||||
TagTreeNodeDTO parent = new TagTreeNodeDTO(parentId, "Immobilie", "teal", 0, List.of(child));
|
TagTreeNodeDTO parent = new TagTreeNodeDTO(parentId, "Immobilie", "teal", 0, List.of(child), null);
|
||||||
when(tagService.getTagTree()).thenReturn(List.of(parent));
|
when(tagService.getTagTree()).thenReturn(List.of(parent));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/tags/tree"))
|
mockMvc.perform(get("/api/tags/tree"))
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ class TagServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void getTagTree_returnsEmptyList_whenNoTags() {
|
void getTagTree_returnsEmptyList_whenNoTags() {
|
||||||
when(tagRepository.findAll()).thenReturn(List.of());
|
when(tagRepository.findAll()).thenReturn(List.of());
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of());
|
||||||
|
|
||||||
assertThat(tagService.getTagTree()).isEmpty();
|
assertThat(tagService.getTagTree()).isEmpty();
|
||||||
}
|
}
|
||||||
@@ -210,6 +211,7 @@ class TagServiceTest {
|
|||||||
Tag.builder().id(idB).name("Beta").build()
|
Tag.builder().id(idB).name("Beta").build()
|
||||||
);
|
);
|
||||||
when(tagRepository.findAll()).thenReturn(tags);
|
when(tagRepository.findAll()).thenReturn(tags);
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of());
|
||||||
|
|
||||||
var tree = tagService.getTagTree();
|
var tree = tagService.getTagTree();
|
||||||
|
|
||||||
@@ -224,6 +226,7 @@ class TagServiceTest {
|
|||||||
Tag parent = Tag.builder().id(parentId).name("Parent").build();
|
Tag parent = Tag.builder().id(parentId).name("Parent").build();
|
||||||
Tag child = Tag.builder().id(childId).name("Child").parentId(parentId).build();
|
Tag child = Tag.builder().id(childId).name("Child").parentId(parentId).build();
|
||||||
when(tagRepository.findAll()).thenReturn(List.of(parent, child));
|
when(tagRepository.findAll()).thenReturn(List.of(parent, child));
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of());
|
||||||
|
|
||||||
var tree = tagService.getTagTree();
|
var tree = tagService.getTagTree();
|
||||||
|
|
||||||
@@ -233,6 +236,44 @@ class TagServiceTest {
|
|||||||
assertThat(tree.get(0).children().get(0).id()).isEqualTo(childId);
|
assertThat(tree.get(0).children().get(0).id()).isEqualTo(childId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── getTagTree (enriched) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTagTree_populatesParentId_onChildNode() {
|
||||||
|
UUID parentId = UUID.randomUUID();
|
||||||
|
UUID childId = UUID.randomUUID();
|
||||||
|
Tag parent = Tag.builder().id(parentId).name("Parent").build();
|
||||||
|
Tag child = Tag.builder().id(childId).name("Child").parentId(parentId).build();
|
||||||
|
when(tagRepository.findAll()).thenReturn(List.of(parent, child));
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of());
|
||||||
|
|
||||||
|
var tree = tagService.getTagTree();
|
||||||
|
|
||||||
|
assertThat(tree.get(0).children().get(0).parentId()).isEqualTo(parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTagTree_populatesDocumentCount_fromAggregateQuery() {
|
||||||
|
UUID tagId = UUID.randomUUID();
|
||||||
|
Tag tag = Tag.builder().id(tagId).name("Tag").build();
|
||||||
|
when(tagRepository.findAll()).thenReturn(List.of(tag));
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.<Object[]>of(new Object[]{tagId, 5L}));
|
||||||
|
|
||||||
|
var tree = tagService.getTagTree();
|
||||||
|
|
||||||
|
assertThat(tree.get(0).documentCount()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTagTree_callsFindDocumentCountsPerTag_exactlyOnce() {
|
||||||
|
when(tagRepository.findAll()).thenReturn(List.of());
|
||||||
|
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of());
|
||||||
|
|
||||||
|
tagService.getTagTree();
|
||||||
|
|
||||||
|
verify(tagRepository, times(1)).findDocumentCountsPerTag();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── resolveEffectiveColors ───────────────────────────────────────────────
|
// ─── resolveEffectiveColors ───────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user