perf: Optimize user creation and deletion#22936
Open
jason-p-pickering wants to merge 39 commits intomasterfrom
Open
perf: Optimize user creation and deletion#22936jason-p-pickering wants to merge 39 commits intomasterfrom
jason-p-pickering wants to merge 39 commits intomasterfrom
Conversation
…-core into fix-usergroup-member-loading
jbee
reviewed
Feb 11, 2026
dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserGroupStore.java
Outdated
Show resolved
Hide resolved
...dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/SchemaToDataFetcher.java
Outdated
Show resolved
Hide resolved
...dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/SchemaToDataFetcher.java
Outdated
Show resolved
Hide resolved
618358a to
bec4474
Compare
…up-member-loading
This reverts commit 6e8d004.
* Invert UserRole/User Hibernate mapping
…up-member-loading
netroms
approved these changes
Feb 17, 2026
…-core into fix-usergroup-member-loading
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Fixes N+1 query performance problems when creating/updating users in systems with large numbers of users and user groups originally reported in https://dhis2.atlassian.net/browse/DHIS2-20614
Problem
Glowroot profiling on a production-scale database identified two major performance bottlenecks when creating a single user via the User app:
SchemaToDataFetcher loading ALL users.: During metadata import, uniqueness checking loaded all 221,647 users from the database (SELECT code, ldapid, username FROM userinfo - 75.6ms, 221K rows), even when importing just one user.
UserGroup member collection loading : Adding/removing users from groups called userGroup.addUser(user) / userGroup.removeUser(user), which triggered lazy initialization of the entire members collection. Similarly, updateUserGroups called userGroup.getMembers().size() to detect changes, also forcing full collection loading. This resulted in SELECT members0_.usergroupid ... FROM usergroupmembers INNER JOIN userinfo executing twice, loading ~12,500 member rows each time (161.4ms total).
Combined, creating a single user took 4,083ms CPU time and allocated 2.7 GB of memory. During production loading, these figures were observed to be much higher.
Both deletion handlers now use direct SQL to manage the join tables via removeAllMemberships(), with updateLastUpdated calls to maintain timestamps and evict L1/L2 caches.
Additionally, the Hibernate mapping ownership for User↔UserRole was inverted to align with User↔UserGroup semantics: User.userRoles is now inverse="true" (non-owning) and UserRole.members is the owning side. Previously, User.userRoles was the owning side with cascade="all", which caused Hibernate to manage the join table through the User entity - triggering cascade-driven chain loading during user deletion.
The @Property(owner = TRUE) annotation override on getUserRoles() ensures the DHIS2 schema/import framework still treats it as an owner property, so that resetNonOwnerProperties() doesn't clear userRoles during PATCH/PUT and collectObjectReferences() still collects role references. The join table is now synced via SQL in UserObjectBundleHook.postCommit().
Solution
SchemaToDataFetcherAdded a filtered
fetch(Schema, Collection)method that only queries for records matching unique property values being imported, using WHERE field IN (:values) clauses instead of loading all records. The old fetch(Schema) method was removed.UserGroupServiceReplaced Hibernate collection operations (userGroup.addUser()/removeUser()/getMembers().size()) with direct SQL via new UserGroupStore methods:
This avoids initializing the lazy members collection entirely.
The updateUserGroups method was rewritten to track membership changes via a changedGroups set rather than comparing getMembers().size() before and after.
canAddOrRemoveMemberAdded an overload accepting UserGroup directly to avoid redundant database lookups when the caller already has the entity loaded. The original code called canAddOrRemoveMember(uid) (which fetched the group by UID), then immediately
called getUserGroup(uid) again.
Additional fixes
Results
Glowroot traces for creating a single user with group assignment on a copy of the database where this problem was originally observed.
Gatling simulation test was performed and the feature branch was consistently faster across the board. 2000 users were imported into the Sierra Leone database for this test.
Mean Response Time (ms)
95th Percentile Response Time (ms)
⬇️ = faster (improvement), ⬆️ = slower (regression)
Known limitation
The direct SQL operations bypass Hibernate's event lifecycle, which means
PostCacheEventPublisherdoes not fire for these changes. In multi-instance deployments with Redis cache invalidation enabled (redis.cache.invalidation.enabled=ON), other nodes will not receive cache invalidation messages for usergroupmembers changes. This will be addressed in a follow-up ticket. Single-instance deployments are unaffected — local L1/L2 caches are explicitly evicted.
Testing
All existing tests pass. Tested with Glowroot on a copy of the production database where the problem was originally observed.
AI Disclaimer:
Portions of the PR were developed with the assistance of AI.