perf: resolve managed-group ids in one query for createUserDetails#24097
perf: resolve managed-group ids in one query for createUserDetails#24097jason-p-pickering wants to merge 4 commits into
Conversation
UserDetails.createUserDetails(User) built managedGroupLongIds from User.getManagedGroups(), which flat-maps group.getManagedGroups() over every group the user belongs to. Each is a lazy collection, so this initialises one usergroupmanaged query per group (an N+1 over the user's groups) on every UserDetails construction from an entity. Resolve the managed-group primary keys in a single SQL query against the usergroupmanaged join table instead. The consumer only needs Long PKs, so no UserGroup entities are hydrated and no join to usergroup is required. - UserGroupStore.getManagedGroupIds(Collection<Long>) + Hibernate impl - UserGroupService.getManagedGroupIds(...) delegate (keeps DefaultUserService's constructor unchanged) - UserDetails.createUserDetails overload taking pre-resolved managedGroupLongIds; null falls back to the entity walk so fromUser, fromUserDontLoadOrgUnits and ApiTokenAuthManager are unchanged - DefaultUserService.createUserDetails passes the resolved ids in Bounded win (scales with groups-per-user, not total users). Reads committed state via JDBC, consistent with the existing HibernateUserGroupStore SQL bypasses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #24097 +/- ##
============================================
- Coverage 69.04% 60.50% -8.54%
+ Complexity 709 619 -90
============================================
Files 3684 3686 +2
Lines 141646 141968 +322
Branches 16453 16511 +58
============================================
- Hits 97797 85897 -11900
- Misses 36243 48971 +12728
+ Partials 7606 7100 -506
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 801 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
|
Marking this as draft. The SonarQube warning is a hint here. We basically went over the edge from 7-> 8 params. We have a total hodge-podge of pre-resolved entities which are passed in, and others which are resolved inside the factory. Maybe its better we make a decision where this is actually done? |
|




Background
While analysing the slow
PATCH /api/users/{uid}traces, we confirmed that the 93s production cliff (Nigeria, ~300k users) is already fixed on master by #23103, #23123, #23126 — those removed the unboundedUserRole.members/UserGroup.membersloads. A fresh v44 master trace on Sierra Leone (PATCH, 209ms, 76 jdbc queries) shows no query now scales with total users.That trace did surface a bounded N+1 this PR removes:
Root cause
UserDetails.createUserDetails(User)buildsmanagedGroupLongIdsfromUser.getManagedGroups():group.getManagedGroups()is a lazy collection, so this initialises oneusergroupmanagedquery per group the user belongs to — on everyUserDetailsconstruction from an entity (principal building, metadata import), not just PATCH.Change
Resolve the managed-group primary keys in a single SQL query against the join table. The consumer (
managedGroupLongIds, aSet<Long>) only needs PKs, so noUserGroupentities are hydrated and nousergroupjoin is needed:UserGroupStore.getManagedGroupIds(Collection<Long>)+HibernateUserGroupStoreimplUserGroupService.getManagedGroupIds(...)delegate — keepsDefaultUserService's constructor unchangedUserDetails.createUserDetails(...)overload accepting pre-resolvedmanagedGroupLongIds;nullfalls back to the entity walk, sofromUser/fromUserDontLoadOrgUnits/ApiTokenAuthManagerare unchangedDefaultUserService.createUserDetails(User)computes the PKs from the already-loadeduser.getGroups()and passes them inScope / risk
UserDetailsconstruction.HibernateUserGroupStoreSQL bypasses; introduces nothing new. The bypass is confined to principal construction — the deletion handler that mutatesmanagedGroupsstill uses ORM.Tests
UserGroupServiceTest.testGetManagedGroupIds/…EmptyInputReturnsEmpty— store method (TDD).UserServiceTest.testCreateUserDetailsResolvesManagedGroupLongIds— characterization provingcreateUserDetails(...).getManagedGroupLongIds()is unchanged through the new path.UserServiceTest.testManagedGroupsregression — still green.🤖 Generated with Claude Code