Taxonomy: Use get_terms instead of a database lookup in term_exists().

Replace raw SQL queries to the terms table, with a call to the `get_terms` function. Using `get_terms` means that `term_exists` is now cached. For developers using `term_exists` where cache invalidation is disabled, such as importing, a workaround was added to ensure that queries are uncached. 

Props Spacedmonkey, boonebgorges, flixos90, peterwilsoncc. 
Fixes #36949. 


git-svn-id: https://develop.svn.wordpress.org/trunk@52921 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Jonny Harris 2022-03-11 11:05:02 +00:00
parent 1bcd722fd6
commit 34d46cd501
5 changed files with 186 additions and 43 deletions

View File

@ -1519,8 +1519,9 @@ function unregister_term_meta( $taxonomy, $meta_key ) {
* Conditional Tags} article in the Theme Developer Handbook.
*
* @since 3.0.0
* @since 6.0.0 Converted to use `get_terms()`.
*
* @global wpdb $wpdb WordPress database abstraction object.
* @global bool $_wp_suspend_cache_invalidation
*
* @param int|string $term The term to check. Accepts term ID, slug, or name.
* @param string $taxonomy Optional. The taxonomy name to use.
@ -1531,65 +1532,71 @@ function unregister_term_meta( $taxonomy, $meta_key ) {
* Returns 0 if term ID 0 is passed to the function.
*/
function term_exists( $term, $taxonomy = '', $parent = null ) {
global $wpdb;
global $_wp_suspend_cache_invalidation;
if ( null === $term ) {
return null;
}
$select = "SELECT term_id FROM $wpdb->terms as t WHERE ";
$tax_select = "SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE ";
$defaults = array(
'get' => 'all',
'fields' => 'ids',
'number' => 1,
'update_term_meta_cache' => false,
'order' => 'ASC',
'orderby' => 'term_id',
'suppress_filter' => true,
);
// Ensure that while importing, queries are not cached.
if ( ! empty( $_wp_suspend_cache_invalidation ) ) {
// @todo Disable caching once #52710 is merged.
$defaults['cache_domain'] = microtime();
}
if ( ! empty( $taxonomy ) ) {
$defaults['taxonomy'] = $taxonomy;
$defaults['fields'] = 'all';
}
if ( is_int( $term ) ) {
if ( 0 === $term ) {
return 0;
}
$where = 't.term_id = %d';
if ( ! empty( $taxonomy ) ) {
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
return $wpdb->get_row( $wpdb->prepare( $tax_select . $where . ' AND tt.taxonomy = %s', $term, $taxonomy ), ARRAY_A );
} else {
return $wpdb->get_var( $wpdb->prepare( $select . $where, $term ) );
$args = wp_parse_args( array( 'include' => array( $term ) ), $defaults );
$terms = get_terms( $args );
} else {
$term = trim( wp_unslash( $term ) );
if ( '' === $term ) {
return null;
}
if ( ! empty( $taxonomy ) && is_numeric( $parent ) ) {
$defaults['parent'] = (int) $parent;
}
$args = wp_parse_args( array( 'slug' => sanitize_title( $term ) ), $defaults );
$terms = get_terms( $args );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
$args = wp_parse_args( array( 'name' => $term ), $defaults );
$terms = get_terms( $args );
}
}
$term = trim( wp_unslash( $term ) );
$slug = sanitize_title( $term );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return null;
}
$_term = array_shift( $terms );
$where = 't.slug = %s';
$else_where = 't.name = %s';
$where_fields = array( $slug );
$else_where_fields = array( $term );
$orderby = 'ORDER BY t.term_id ASC';
$limit = 'LIMIT 1';
if ( ! empty( $taxonomy ) ) {
if ( is_numeric( $parent ) ) {
$parent = (int) $parent;
$where_fields[] = $parent;
$else_where_fields[] = $parent;
$where .= ' AND tt.parent = %d';
$else_where .= ' AND tt.parent = %d';
}
$where_fields[] = $taxonomy;
$else_where_fields[] = $taxonomy;
$result = $wpdb->get_row( $wpdb->prepare( "SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE $where AND tt.taxonomy = %s $orderby $limit", $where_fields ), ARRAY_A );
if ( $result ) {
return $result;
}
return $wpdb->get_row( $wpdb->prepare( "SELECT tt.term_id, tt.term_taxonomy_id FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy as tt ON tt.term_id = t.term_id WHERE $else_where AND tt.taxonomy = %s $orderby $limit", $else_where_fields ), ARRAY_A );
return array(
'term_id' => (string) $_term->term_id,
'term_taxonomy_id' => (string) $_term->term_taxonomy_id,
);
}
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$result = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->terms as t WHERE $where $orderby $limit", $where_fields ) );
if ( $result ) {
return $result;
}
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->terms as t WHERE $else_where $orderby $limit", $else_where_fields ) );
return (string) $_term;
}
/**

View File

@ -31,6 +31,8 @@ class Tests_Term_GetTerm extends WP_UnitTestCase {
array( '%d' )
);
clean_term_cache( $t1['term_id'] );
return array(
array(
'term_id' => $t1['term_id'],

View File

@ -54,6 +54,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( '%d' ),
array( '%d' )
);
clean_term_cache( $t1['term_id'], 'category' );
$t2_child = wp_insert_term(
'Foo Child',
@ -151,6 +152,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( '%d' ),
array( '%d' )
);
clean_term_cache( $t1['term_id'], 'category' );
$th = _get_term_hierarchy( 'wptests_tax_4' );
$new_term_id = _split_shared_term( $t1['term_id'], $t3['term_taxonomy_id'] );
@ -179,6 +181,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( '%d' ),
array( '%d' )
);
clean_term_cache( $t1['term_id'], 'category' );
$this->assertSame( $t1['term_id'], get_option( 'default_category', -1 ) );
@ -207,6 +210,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( '%d' ),
array( '%d' )
);
clean_term_cache( $t1['term_id'], 'category' );
$menu_id = wp_create_nav_menu( 'Nav Menu Bar' );
$cat_menu_item = wp_update_nav_menu_item(
@ -245,6 +249,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( 'term_id' => $shared_term_id ),
array( 'term_taxonomy_id' => $nav_term->term_taxonomy_id )
);
clean_term_cache( $shared_term_id, 'category' );
set_theme_mod( 'nav_menu_locations', array( 'foo' => $shared_term_id ) );
@ -274,6 +279,7 @@ class Tests_Term_SplitSharedTerm extends WP_UnitTestCase {
array( 'term_id' => $shared_term_id ),
array( 'term_taxonomy_id' => $nav_term->term_taxonomy_id )
);
clean_term_cache( $shared_term_id, 'category' );
$t1 = wp_insert_term( 'Random term', 'category' );
$cat_menu_item = wp_update_nav_menu_item(

View File

@ -271,6 +271,132 @@ class Tests_TermExists extends WP_UnitTestCase {
_unregister_taxonomy( 'wptests_tax' );
}
/**
* @ticket 36949
* @covers ::term_exists()
*/
public function test_term_lookup_by_id_and_update() {
register_taxonomy( 'wptests_tax', 'post' );
$slug = __FUNCTION__;
$t = self::factory()->term->create(
array(
'slug' => $slug,
'taxonomy' => 'wptests_tax',
)
);
$this->assertEquals( $t, term_exists( $t ) );
$this->assertTrue( wp_delete_term( $t, 'wptests_tax' ) );
$this->assertNull( term_exists( $t ) );
// Clean up.
_unregister_taxonomy( 'wptests_tax' );
}
/**
* @ticket 36949
* @covers ::term_exists()
*/
public function test_term_lookup_by_slug_and_update() {
register_taxonomy( 'wptests_tax', 'post' );
$slug = __FUNCTION__;
$t = self::factory()->term->create(
array(
'slug' => $slug,
'taxonomy' => 'wptests_tax',
)
);
$this->assertEquals( $t, term_exists( $slug ) );
$this->assertTrue( wp_delete_term( $t, 'wptests_tax' ) );
$this->assertNull( term_exists( $slug ) );
// Clean up.
_unregister_taxonomy( 'wptests_tax' );
}
/**
* @ticket 36949
* @covers ::term_exists()
*/
public function test_term_exists_caching() {
global $wpdb;
register_taxonomy( 'wptests_tax', 'post' );
$slug = __FUNCTION__;
$t = self::factory()->term->create(
array(
'slug' => $slug,
'taxonomy' => 'wptests_tax',
)
);
$this->assertEquals( $t, term_exists( $slug ) );
$num_queries = $wpdb->num_queries;
$this->assertEquals( $t, term_exists( $slug ) );
$this->assertSame( $num_queries, $wpdb->num_queries );
$this->assertTrue( wp_delete_term( $t, 'wptests_tax' ) );
$num_queries = $wpdb->num_queries;
$this->assertNull( term_exists( $slug ) );
$this->assertSame( $num_queries + 2, $wpdb->num_queries );
// Clean up.
_unregister_taxonomy( 'wptests_tax' );
}
/**
* @ticket 36949
* @covers ::term_exists()
*/
public function test_term_exists_caching_suspend_cache_invalidation() {
global $wpdb;
register_taxonomy( 'wptests_tax', 'post' );
wp_suspend_cache_invalidation( true );
$slug = __FUNCTION__;
$t = self::factory()->term->create(
array(
'slug' => $slug,
'taxonomy' => 'wptests_tax',
)
);
$this->assertEquals( $t, term_exists( $slug ) );
$num_queries = $wpdb->num_queries;
$this->assertEquals( $t, term_exists( $slug ) );
$this->assertSame( $num_queries + 1, $wpdb->num_queries );
wp_suspend_cache_invalidation( false );
// Clean up.
_unregister_taxonomy( 'wptests_tax' );
}
/**
* @ticket 36949
* @covers ::term_exists()
*/
public function test_term_exists_caching_by_int_suspend_cache_invalidation() {
register_taxonomy( 'wptests_tax', 'post' );
$slug = __FUNCTION__;
$t = self::factory()->term->create(
array(
'slug' => $slug,
'taxonomy' => 'wptests_tax',
)
);
// Warm cache in get_term() via term_exists().
term_exists( $t );
wp_suspend_cache_invalidation( true );
wp_delete_term( $t, 'wptests_tax' );
$this->assertNull( term_exists( $t ) );
// Reneable cache invalidation.
wp_suspend_cache_invalidation( false );
_unregister_taxonomy( 'wptests_tax' );
}
public function test_term_exists_unknown() {
$this->assertNull( term_exists( rand_str() ) );
$this->assertSame( 0, term_exists( 0 ) );

View File

@ -447,6 +447,8 @@ class Tests_Term_WpGetObjectTerms extends WP_UnitTestCase {
$wpdb->update( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => 100006 ), array( 'term_taxonomy_id' => $term_2->term_taxonomy_id ) );
$wpdb->update( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => 100005 ), array( 'term_taxonomy_id' => $term_3->term_taxonomy_id ) );
clean_term_cache( array( $t1, $t2, $t3 ), $this->taxonomy );
$set = wp_set_object_terms( $p, array( $t1, $t2, $t3 ), $this->taxonomy );
$found = wp_get_object_terms(