Skip to content

Commit 00d068b

Browse files
Cache API: Cache non-existent users in WP_User::get_data_by() to prevent duplicate queries.
When get_userdata() or WP_User::get_data_by('id', $id) is called for a non-existent user, the result is now cached in a 'notusers' array within the 'users' cache group, following the same pattern as 'notoptions' in the options API. Subsequent calls return false immediately without hitting the database. The cache is invalidated by update_user_caches() whenever a user is created or updated. Fixes #46388. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent adf6443 commit 00d068b

3 files changed

Lines changed: 134 additions & 0 deletions

File tree

src/wp-includes/class-wp-user.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ public function init( $data, $site_id = 0 ) {
190190
*
191191
* @since 3.3.0
192192
* @since 4.4.0 Added 'ID' as an alias of 'id' for the `$field` parameter.
193+
* @since 7.1.0 Non-existent users looked up by ID are now cached to prevent duplicate queries.
193194
*
194195
* @global wpdb $wpdb WordPress database abstraction object.
195196
*
@@ -251,13 +252,27 @@ public static function get_data_by( $field, $value ) {
251252
}
252253
}
253254

255+
if ( 'id' === $field ) {
256+
$notusers = wp_cache_get( 'notusers', 'users' );
257+
if ( is_array( $notusers ) && isset( $notusers[ $user_id ] ) ) {
258+
return false;
259+
}
260+
}
261+
254262
$user = $wpdb->get_row(
255263
$wpdb->prepare(
256264
"SELECT * FROM $wpdb->users WHERE $db_field = %s LIMIT 1",
257265
$value
258266
)
259267
);
260268
if ( ! $user ) {
269+
if ( 'id' === $field ) {
270+
if ( ! is_array( $notusers ) ) {
271+
$notusers = array();
272+
}
273+
$notusers[ $user_id ] = true;
274+
wp_cache_set( 'notusers', $notusers, 'users' );
275+
}
261276
return false;
262277
}
263278

src/wp-includes/user.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,7 @@ function sanitize_user_field( $field, $value, $user_id, $context ) {
19851985
* Updates all user caches.
19861986
*
19871987
* @since 3.0.0
1988+
* @since 7.1.0 The `notusers` cache is cleared for the user's ID.
19881989
*
19891990
* @param object|WP_User $user User object or database row to be cached
19901991
* @return void|false Void on success, false on failure.
@@ -2005,6 +2006,12 @@ function update_user_caches( $user ) {
20052006
if ( ! empty( $user->user_email ) ) {
20062007
wp_cache_add( $user->user_email, $user->ID, 'useremail' );
20072008
}
2009+
2010+
$notusers = wp_cache_get( 'notusers', 'users' );
2011+
if ( is_array( $notusers ) && isset( $notusers[ $user->ID ] ) ) {
2012+
unset( $notusers[ $user->ID ] );
2013+
wp_cache_set( 'notusers', $notusers, 'users' );
2014+
}
20082015
}
20092016

20102017
/**
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/**
4+
* Tests for WP_User::get_data_by() caching behavior.
5+
*
6+
* @group user
7+
* @group cache
8+
*
9+
* @coversDefaultClass WP_User
10+
*/
11+
class Tests_User_GetDataBy extends WP_UnitTestCase {
12+
13+
/**
14+
* @ticket 46388
15+
* @covers WP_User::get_data_by
16+
*/
17+
public function test_nonexistent_user_by_id_does_not_trigger_multiple_queries() {
18+
global $wpdb;
19+
20+
$nonexistent_id = PHP_INT_MAX;
21+
22+
// Ensure the ID is not cached from a previous test.
23+
$notusers = wp_cache_get( 'notusers', 'users' );
24+
if ( is_array( $notusers ) ) {
25+
unset( $notusers[ $nonexistent_id ] );
26+
wp_cache_set( 'notusers', $notusers, 'users' );
27+
}
28+
wp_cache_delete( $nonexistent_id, 'users' );
29+
30+
$before = $wpdb->num_queries;
31+
get_userdata( $nonexistent_id );
32+
$after_first_call = $wpdb->num_queries;
33+
34+
get_userdata( $nonexistent_id );
35+
$after_second_call = $wpdb->num_queries;
36+
37+
$this->assertSame( 1, $after_first_call - $before, 'First call for non-existent user should trigger one DB query.' );
38+
$this->assertSame( 0, $after_second_call - $after_first_call, 'Second call for non-existent user should not trigger any DB queries.' );
39+
}
40+
41+
/**
42+
* @ticket 46388
43+
* @covers WP_User::get_data_by
44+
*/
45+
public function test_nonexistent_user_by_id_is_added_to_notusers_cache() {
46+
$nonexistent_id = PHP_INT_MAX - 1;
47+
48+
// Clear any existing state.
49+
$notusers = wp_cache_get( 'notusers', 'users' );
50+
if ( is_array( $notusers ) ) {
51+
unset( $notusers[ $nonexistent_id ] );
52+
wp_cache_set( 'notusers', $notusers, 'users' );
53+
}
54+
wp_cache_delete( $nonexistent_id, 'users' );
55+
56+
get_userdata( $nonexistent_id );
57+
58+
$notusers = wp_cache_get( 'notusers', 'users' );
59+
60+
$this->assertIsArray( $notusers, 'notusers cache should be an array after a non-existent user lookup.' );
61+
$this->assertArrayHasKey( $nonexistent_id, $notusers, 'Non-existent user ID should be stored in notusers cache.' );
62+
}
63+
64+
/**
65+
* @ticket 46388
66+
* @covers WP_User::get_data_by
67+
*/
68+
public function test_notusers_cache_is_invalidated_when_user_is_created() {
69+
global $wpdb;
70+
71+
// Manually prime the notusers cache for a specific ID.
72+
$user_id = self::factory()->user->create();
73+
$notusers = wp_cache_get( 'notusers', 'users' );
74+
if ( ! is_array( $notusers ) ) {
75+
$notusers = array();
76+
}
77+
$notusers[ $user_id ] = true;
78+
wp_cache_set( 'notusers', $notusers, 'users' );
79+
80+
// Also clear the positive users cache to force a real test.
81+
wp_cache_delete( $user_id, 'users' );
82+
83+
// update_user_caches is called by wp_insert_user / wp_update_user.
84+
// Simulate what happens when user data is refreshed.
85+
$raw_user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE ID = %d", $user_id ) );
86+
update_user_caches( $raw_user );
87+
88+
$notusers = wp_cache_get( 'notusers', 'users' );
89+
90+
$this->assertFalse(
91+
is_array( $notusers ) && isset( $notusers[ $user_id ] ),
92+
'User ID should be removed from notusers cache after update_user_caches() is called.'
93+
);
94+
}
95+
96+
/**
97+
* @ticket 46388
98+
* @covers WP_User::get_data_by
99+
*/
100+
public function test_existing_user_by_id_is_not_added_to_notusers_cache() {
101+
$user_id = self::factory()->user->create();
102+
103+
get_userdata( $user_id );
104+
105+
$notusers = wp_cache_get( 'notusers', 'users' );
106+
107+
$this->assertFalse(
108+
is_array( $notusers ) && isset( $notusers[ $user_id ] ),
109+
'Existing user ID should not be stored in notusers cache.'
110+
);
111+
}
112+
}

0 commit comments

Comments
 (0)