Skip to content

[2.x] Fix N+1 group_user queries when serializing users - #4696

Merged
imorland merged 2 commits into
2.xfrom
im/fix-4695-user-groups-n-plus-one
Jun 7, 2026
Merged

[2.x] Fix N+1 group_user queries when serializing users#4696
imorland merged 2 commits into
2.xfrom
im/fix-4695-user-groups-n-plus-one

Conversation

@imorland

@imorland imorland commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

Serializing users through UserResource issued redundant group_user queries. The groups relationship getter called $user->groups()->get() / $user->visibleGroups()->get(), so a fresh query ran for every serialized user whenever groups were in the payload (e.g. user.groups on the posts endpoint), and the count grew linearly with the number of users.

A secondary, constant inefficiency came from the email field's visibility check (editCredentialsUser::isAdmin()), which reads $user->groups. Because the getter queried a different relation, each user's groups were loaded twice.

Changes

  • UserResource groups getter now reads the (eager-)loaded groups relation and filters hidden groups in PHP, reusing the relation isAdmin() already loads. values() keeps the filtered result a JSON array rather than an object.
  • Eager-loading added so the relation is batch-loaded in a single query: groups on UserResource Update/Show/Index, user.groups on PostResource Show/Index, and user.groups/lastPostedUser.groups/firstPost.user.groups on DiscussionResource::Show (to match Index).
  • email / isEmailConfirmed visibility reordered to check the cheap id === id self comparison before the editCredentials policy, avoiding an isAdmin() groups read for the own-profile case.

Reviewer notes

Hidden-group filtering is preserved: actors without viewHiddenGroups still never see hidden groups in the relationship or includes (covered by the existing ShowGroupsRelationshipTest, hardened here with a hidden group sorted first + a JSON-array assertion). Behaviour for the linkage is unchanged.

Fixes #4695

The UserResource `groups` relationship getter issued a fresh `group_user`
query for every serialized user, so the query count grew linearly with the
number of users in a payload (e.g. one per post author on the posts endpoint).

The getter now reads the (eager-)loaded `groups` relation and filters hidden
groups in PHP, and the relevant endpoints eager-load `groups` so it is batched
into a single query. This also shares the relation that `User::isAdmin()`
reads (via the `email` field's `editCredentials` check), removing the
redundant per-user load, and the email/isEmailConfirmed visibility checks now
short-circuit on the cheap self comparison before invoking the policy.

Fixes #4695
@imorland
imorland requested a review from a team as a code owner June 7, 2026 21:39
@imorland imorland added this to the 2.0.0-rc.3 milestone Jun 7, 2026
@imorland imorland changed the title Fix N+1 group_user queries when serializing users [2.x] Fix N+1 group_user queries when serializing users Jun 7, 2026
During fulltext search the index populates mostRelevantPost.user, whose
isAdmin() check would otherwise lazy-load groups per discussion.
@imorland
imorland merged commit 8f89c17 into 2.x Jun 7, 2026
25 checks passed
@imorland
imorland deleted the im/fix-4695-user-groups-n-plus-one branch June 7, 2026 22:43
imorland added a commit that referenced this pull request Jun 13, 2026
UserPolicy::editCredentials evaluated $user->isAdmin() (which reads
$user->groups) before checking whether the actor can edit credentials at
all. On render paths that serialize many users — post authors, and likers
via flarum-likes' default `likes` include — UserResource's email-field
visibility runs this check per serialized user, lazy-loading each one's
groups: an N+1 that #4696 did not cover.

Check the actor's `user.editCredentials` permission first and return no
opinion when absent (guests and normal users — the common case), so the
target user's groups are never touched. $user->isAdmin() is only consulted
when the actor can actually edit credentials, where it is needed to protect
admins. The visibility outcome is unchanged in every case.

Adds a flarum-likes regression test asserting likers' groups are not
loaded one query per liker.

Fixes #4724.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

N+1 group_user queries: UserResource 'groups' getter ignores eager-loaded relation (re-queries per serialized user)

1 participant