Skip to content

Commit deaba3c

Browse files
authored
fix: only generate links when fields expand to href [DHIS2-21856] (2.43) (#24501)
Single-object GET treated any field preset (e.g. :owner) as if href was requested, triggering deep link generation that reflectively initializes every lazy collection of the entity. On objects with very large collections (e.g. a user role with 240k members) this loads and L2-caches every member entity, taking 30+ seconds of CPU per request. Only *, :all and :simple actually expand to the transient href property, so only those (and a literal href field) now trigger link generation. Response payloads are unchanged for all field values. AI Assisted
1 parent 31819c1 commit deaba3c

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractCrudControllerTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
import org.hisp.dhis.user.UserGroup;
7676
import org.junit.jupiter.api.DisplayName;
7777
import org.junit.jupiter.api.Test;
78+
import org.junit.jupiter.params.ParameterizedTest;
79+
import org.junit.jupiter.params.provider.ValueSource;
7880
import org.springframework.http.MediaType;
7981
import org.springframework.transaction.annotation.Transactional;
8082

@@ -105,6 +107,28 @@ void testGetObject() {
105107
assertEquals(id, userById.getId());
106108
}
107109

110+
@ParameterizedTest
111+
@ValueSource(strings = {":all", "*", ":simple", "href", "id,href", "id,name,href"})
112+
@DisplayName("Single object GET returns href when requested directly or via an expanding preset")
113+
void testGetObjectWithHref(String fields) {
114+
String id = GET("/users/").content().getList("users", JsonUser.class).get(0).getId();
115+
JsonUser user =
116+
GET("/users/{id}?fields={fields}", id, fields).content(HttpStatus.OK).as(JsonUser.class);
117+
String href = user.getString("href").string();
118+
assertNotNull(href);
119+
assertTrue(href.endsWith("/users/" + id));
120+
}
121+
122+
@ParameterizedTest
123+
@ValueSource(strings = {":owner", ":identifiable", ":nameable", ":persisted", "id,name"})
124+
@DisplayName("Single object GET omits href for presets that do not expand to it")
125+
void testGetObjectWithoutHref(String fields) {
126+
String id = GET("/users/").content().getList("users", JsonUser.class).get(0).getId();
127+
JsonUser user =
128+
GET("/users/{id}?fields={fields}", id, fields).content(HttpStatus.OK).as(JsonUser.class);
129+
assertFalse(user.has("href"));
130+
}
131+
108132
@Test
109133
void testGetObjectProperty() {
110134
// response will look like: { "surname": <name> }

dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,14 @@ private void handleLinksAndAccess(List<T> entityList, List<String> fields, boole
541541

542542
private boolean fieldsContains(String match, List<String> fields) {
543543
for (String field : fields) {
544-
// for now assume href/access if * or preset is requested
545-
if (field.contains(match) || field.equals("*") || field.startsWith(":")) {
544+
// only presets that expand to the href property need link generation;
545+
// href is transient (not owner/persisted) and not part of the fixed
546+
// identifiable/nameable presets, but it is a simple property, so it is
547+
// included by *, :all and :simple
548+
if (field.contains(match)
549+
|| field.equals("*")
550+
|| field.equals(":all")
551+
|| field.equals(":simple")) {
546552
return true;
547553
}
548554
}

0 commit comments

Comments
 (0)