Make WordPress Core

Changeset 52836


Ignore:
Timestamp:
03/10/2022 10:56:09 AM (2 years ago)
Author:
spacedmonkey
Message:

Taxonomy: Only store term_ids and object_ids in WP_Term_Query query caches.

The query cache currently implemented in WP_Term_Query caches the final output of the query, depending on what fields are requested. This is wasteful, as if a user requests fields => all, then an unlimited array of WP_Term objects could be stored in the object cache. Instead of storing the whole WP_Term object, this change only the term_id is stored. To get an array the full WP_Term objects, the _prime_term_caches function is called with an array of ids. In instances where a persistent object cache is not in use, then this will result in another SQL query to be run. After _prime_term_caches is called if this term is requested again in the same page load, then it will already be loaded into memory. If a user runs WP_Term_Query with the fields param set to all_with_object_id, an array of objects containing both the term_id and object_ids are stored in cache.

This change also improves the logic to load term meta caches. This change ensures that term meta is always primed for all terms loaded in the term query.

Props Spacedmonkey, boonebgorges, jbpaul17, peterwilsoncc, flixos90, pbearne.
Fixes #37189.

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-term-query.php

    r52669 r52836  
    637637        $selects = array();
    638638        switch ( $args['fields'] ) {
    639             case 'all':
    640             case 'all_with_object_id':
    641             case 'tt_ids':
    642             case 'slugs':
    643                 $selects = array( 't.*', 'tt.*' );
    644                 if ( 'all_with_object_id' === $args['fields'] && ! empty( $args['object_ids'] ) ) {
    645                     $selects[] = 'tr.object_id';
    646                 }
    647                 break;
    648             case 'ids':
    649             case 'id=>parent':
    650                 $selects = array( 't.term_id', 'tt.parent', 'tt.count', 'tt.taxonomy' );
    651                 break;
    652             case 'names':
    653                 $selects = array( 't.term_id', 'tt.parent', 'tt.count', 't.name', 'tt.taxonomy' );
    654                 break;
    655639            case 'count':
    656640                $orderby = '';
     
    658642                $selects = array( 'COUNT(*)' );
    659643                break;
    660             case 'id=>name':
    661                 $selects = array( 't.term_id', 't.name', 'tt.parent', 'tt.count', 'tt.taxonomy' );
    662                 break;
    663             case 'id=>slug':
    664                 $selects = array( 't.term_id', 't.slug', 'tt.parent', 'tt.count', 'tt.taxonomy' );
     644            default:
     645                $selects = array( 't.term_id' );
     646                if ( 'all_with_object_id' === $args['fields'] && ! empty( $args['object_ids'] ) ) {
     647                    $selects[] = 'tr.object_id';
     648                }
    665649                break;
    666650        }
     
    689673
    690674        if ( ! empty( $this->query_vars['object_ids'] ) ) {
    691             $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id";
     675            $join    .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id";
     676            $distinct = 'DISTINCT';
    692677        }
    693678
     
    749734        $cache        = wp_cache_get( $cache_key, 'terms' );
    750735        if ( false !== $cache ) {
    751             if ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
    752                 $cache = $this->populate_terms( $cache );
     736            if ( 'ids' === $_fields ) {
     737                $term_ids = wp_list_pluck( $cache, 'term_id' );
     738                $cache    = array_map( 'intval', $term_ids );
     739            } elseif ( 'count' !== $_fields ) {
     740                $term_ids = wp_list_pluck( $cache, 'term_id' );
     741                _prime_term_caches( $term_ids, $args['update_term_meta_cache'] );
     742                $term_objects = $this->populate_terms( $cache );
     743                $cache        = $this->format_terms( $term_objects, $_fields );
    753744            }
    754745
     
    758749
    759750        if ( 'count' === $_fields ) {
    760             $count = $wpdb->get_var( $this->request );
     751            $count = $wpdb->get_var( $this->request ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
    761752            wp_cache_set( $cache_key, $count, 'terms' );
    762753            return $count;
    763754        }
    764755
    765         $terms = $wpdb->get_results( $this->request );
    766 
    767         if ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
    768             update_term_cache( $terms );
    769         }
    770 
    771         // Prime termmeta cache.
    772         if ( $args['update_term_meta_cache'] ) {
    773             $term_ids = wp_list_pluck( $terms, 'term_id' );
    774             update_termmeta_cache( $term_ids );
    775         }
     756        $terms = $wpdb->get_results( $this->request ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
    776757
    777758        if ( empty( $terms ) ) {
     
    779760            return array();
    780761        }
     762
     763        $term_ids = wp_list_pluck( $terms, 'term_id' );
     764        _prime_term_caches( $term_ids, false );
     765        $term_objects = $this->populate_terms( $terms );
    781766
    782767        if ( $child_of ) {
     
    784769                $children = _get_term_hierarchy( $_tax );
    785770                if ( ! empty( $children ) ) {
    786                     $terms = _get_term_children( $child_of, $terms, $_tax );
     771                    $term_objects = _get_term_children( $child_of, $term_objects, $_tax );
    787772                }
    788773            }
     
    792777        if ( $args['pad_counts'] && 'all' === $_fields ) {
    793778            foreach ( $taxonomies as $_tax ) {
    794                 _pad_term_counts( $terms, $_tax );
     779                _pad_term_counts( $term_objects, $_tax );
    795780            }
    796781        }
    797782
    798783        // Make sure we show empty categories that have children.
    799         if ( $hierarchical && $args['hide_empty'] && is_array( $terms ) ) {
    800             foreach ( $terms as $k => $term ) {
     784        if ( $hierarchical && $args['hide_empty'] && is_array( $term_objects ) ) {
     785            foreach ( $term_objects as $k => $term ) {
    801786                if ( ! $term->count ) {
    802787                    $children = get_term_children( $term->term_id, $term->taxonomy );
     
    811796
    812797                    // It really is empty.
    813                     unset( $terms[ $k ] );
     798                    unset( $term_objects[ $k ] );
    814799                }
    815800            }
     
    837822        }
    838823
    839         $_terms = array();
    840         if ( 'id=>parent' === $_fields ) {
    841             foreach ( $terms as $term ) {
    842                 $_terms[ $term->term_id ] = $term->parent;
    843             }
    844         } elseif ( 'ids' === $_fields ) {
    845             foreach ( $terms as $term ) {
    846                 $_terms[] = (int) $term->term_id;
    847             }
    848         } elseif ( 'tt_ids' === $_fields ) {
    849             foreach ( $terms as $term ) {
    850                 $_terms[] = (int) $term->term_taxonomy_id;
    851             }
    852         } elseif ( 'names' === $_fields ) {
    853             foreach ( $terms as $term ) {
    854                 $_terms[] = $term->name;
    855             }
    856         } elseif ( 'slugs' === $_fields ) {
    857             foreach ( $terms as $term ) {
    858                 $_terms[] = $term->slug;
    859             }
    860         } elseif ( 'id=>name' === $_fields ) {
    861             foreach ( $terms as $term ) {
    862                 $_terms[ $term->term_id ] = $term->name;
    863             }
    864         } elseif ( 'id=>slug' === $_fields ) {
    865             foreach ( $terms as $term ) {
    866                 $_terms[ $term->term_id ] = $term->slug;
    867             }
    868         }
    869 
    870         if ( ! empty( $_terms ) ) {
    871             $terms = $_terms;
    872         }
    873 
    874824        // Hierarchical queries are not limited, so 'offset' and 'number' must be handled now.
    875825        if ( $hierarchical && $number && is_array( $terms ) ) {
    876826            if ( $offset >= count( $terms ) ) {
    877                 $terms = array();
     827                $terms        = array();
     828                $term_objects = array();
    878829            } else {
    879                 $terms = array_slice( $terms, $offset, $number, true );
    880             }
     830                $terms        = array_slice( $terms, $offset, $number, true );
     831                $term_objects = array_slice( $term_objects, $offset, $number, true );
     832            }
     833        }
     834
     835        // Prime termmeta cache.
     836        if ( $args['update_term_meta_cache'] ) {
     837            $term_ids = wp_list_pluck( $term_objects, 'term_id' );
     838            update_termmeta_cache( $term_ids );
    881839        }
    882840
    883841        wp_cache_add( $cache_key, $terms, 'terms' );
    884 
    885         if ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
    886             $terms = $this->populate_terms( $terms );
    887         }
     842        $terms = $this->format_terms( $term_objects, $_fields );
    888843
    889844        $this->terms = $terms;
     
    951906
    952907    /**
     908     * Format response depending on field requested.
     909     *
     910     * @since 6.0.0
     911     *
     912     * @param WP_Term[] $term_objects Array of term objects.
     913     * @param string $_fields Field to format.
     914     *
     915     * @return WP_Term[]|int[]|string[] Array of terms / strings / ints depending on field requested.
     916     */
     917    protected function format_terms( $term_objects, $_fields ) {
     918        $_terms = array();
     919        if ( 'id=>parent' === $_fields ) {
     920            foreach ( $term_objects as $term ) {
     921                $_terms[ $term->term_id ] = $term->parent;
     922            }
     923        } elseif ( 'ids' === $_fields ) {
     924            foreach ( $term_objects as $term ) {
     925                $_terms[] = (int) $term->term_id;
     926            }
     927        } elseif ( 'tt_ids' === $_fields ) {
     928            foreach ( $term_objects as $term ) {
     929                $_terms[] = (int) $term->term_taxonomy_id;
     930            }
     931        } elseif ( 'names' === $_fields ) {
     932            foreach ( $term_objects as $term ) {
     933                $_terms[] = $term->name;
     934            }
     935        } elseif ( 'slugs' === $_fields ) {
     936            foreach ( $term_objects as $term ) {
     937                $_terms[] = $term->slug;
     938            }
     939        } elseif ( 'id=>name' === $_fields ) {
     940            foreach ( $term_objects as $term ) {
     941                $_terms[ $term->term_id ] = $term->name;
     942            }
     943        } elseif ( 'id=>slug' === $_fields ) {
     944            foreach ( $term_objects as $term ) {
     945                $_terms[ $term->term_id ] = $term->slug;
     946            }
     947        } elseif ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
     948            $_terms = $term_objects;
     949        }
     950
     951        return $_terms;
     952    }
     953
     954    /**
    953955     * Generate the ORDER BY clause for an 'orderby' param that is potentially related to a meta query.
    954956     *
     
    10541056     * @since 4.9.8
    10551057     *
    1056      * @param array $term_ids Term IDs.
    1057      * @return array
    1058      */
    1059     protected function populate_terms( $term_ids ) {
    1060         $terms = array();
    1061 
    1062         if ( ! is_array( $term_ids ) ) {
    1063             return $terms;
    1064         }
    1065 
    1066         foreach ( $term_ids as $key => $term_id ) {
    1067             $term = get_term( $term_id );
     1058     * @param Object[]|int[] $terms List of objects or term ids.
     1059     * @return WP_Term[] Array of `WP_Term` objects.
     1060     */
     1061    protected function populate_terms( $terms ) {
     1062        $term_objects = array();
     1063        if ( ! is_array( $terms ) ) {
     1064            return $term_objects;
     1065        }
     1066
     1067        foreach ( $terms as $key => $term_data ) {
     1068            if ( is_object( $term_data ) && property_exists( $term_data, 'term_id' ) ) {
     1069                $term = get_term( $term_data->term_id );
     1070                if ( property_exists( $term_data, 'object_id' ) ) {
     1071                    $term->object_id = (int) $term_data->object_id;
     1072                }
     1073            } else {
     1074                $term = get_term( $term_data );
     1075            }
     1076
    10681077            if ( $term instanceof WP_Term ) {
    1069                 $terms[ $key ] = $term;
    1070             }
    1071         }
    1072 
    1073         return $terms;
     1078                $term_objects[ $key ] = $term;
     1079            }
     1080        }
     1081
     1082        return $term_objects;
    10741083    }
    10751084}
  • trunk/tests/phpunit/tests/term/cache.php

    r52010 r52836  
    256256        $num_queries = $wpdb->num_queries;
    257257
    258         $term = get_term_by( 'slug', 'burrito', 'post_tag' );
    259         $num_queries++;
     258        $term        = get_term_by( 'slug', 'burrito', 'post_tag' );
     259        $num_queries = $num_queries + 2;
    260260        $this->assertSame( 'Taco', $term->name );
    261261        $this->assertSame( $num_queries, $wpdb->num_queries );
     
    287287        $num_queries = $wpdb->num_queries;
    288288
    289         $term = get_term_by( 'slug', 'burrito', 'post_tag' );
    290         $num_queries++;
     289        $term        = get_term_by( 'slug', 'burrito', 'post_tag' );
     290        $num_queries = $num_queries + 2;
    291291        $this->assertSame( 'Taco', $term->name );
    292292        $this->assertSame( $num_queries, $wpdb->num_queries );
     
    302302
    303303        // This should not hit cache.
    304         $term = get_term_by( 'slug', 'burrito', 'post_tag' );
    305         $num_queries++;
     304        $term        = get_term_by( 'slug', 'burrito', 'post_tag' );
     305        $num_queries = $num_queries + 2;
    306306        $this->assertSame( 'No Taco', $term->name );
    307307        $this->assertSame( $num_queries, $wpdb->num_queries );
     
    326326
    327327        get_term_by( 'name', 'Burrito', 'post_tag' );
    328         $num_queries++;
     328        $num_queries = $num_queries + 2;
    329329        $this->assertSame( $num_queries, $wpdb->num_queries );
    330330
     
    355355
    356356        get_term_by( 'name', 'Burrito', 'post_tag' );
    357         $num_queries++;
     357        $num_queries = $num_queries + 2;
    358358        $this->assertSame( $num_queries, $wpdb->num_queries );
    359359
     
    368368        // This should not hit cache.
    369369        get_term_by( 'name', 'burrito', 'post_tag' );
    370         $num_queries++;
     370        $num_queries = $num_queries + 2;
    371371        $this->assertSame( $num_queries, $wpdb->num_queries );
    372372    }
     
    389389        $last_changed = wp_cache_get( 'last_changed', 'terms' );
    390390
    391         $term1 = get_term_by( 'name', 'Burrito', 'post_tag' );
    392         $num_queries++;
     391        $term1       = get_term_by( 'name', 'Burrito', 'post_tag' );
     392        $num_queries = $num_queries + 2;
    393393
    394394        // Verify the term is cached.
     
    432432        $num_queries = $wpdb->num_queries;
    433433
    434         $term = get_term_by( 'name', 'Burrito', 'post_tag' );
    435         $num_queries++;
     434        $term        = get_term_by( 'name', 'Burrito', 'post_tag' );
     435        $num_queries = $num_queries + 2;
    436436        $this->assertInstanceOf( 'WP_Term', $term );
    437437        $this->assertSame( $term_id, $term->term_id );
  • trunk/tests/phpunit/tests/term/getTermBy.php

    r52010 r52836  
    55 */
    66class Tests_Term_GetTermBy extends WP_UnitTestCase {
     7
     8    protected $query = '';
    79
    810    public function test_get_term_by_slug() {
     
    124126        $num_queries = $wpdb->num_queries;
    125127        $found       = get_term_by( 'slug', 'foo', 'wptests_tax' );
    126         $num_queries++;
     128        $num_queries = $num_queries + 2;
    127129
    128130        $this->assertInstanceOf( 'WP_Term', $found );
     
    210212     */
    211213    public function test_query_should_contain_limit_clause() {
    212         global $wpdb;
    213 
    214214        $term_id = $this->factory->term->create(
    215215            array(
     
    218218            )
    219219        );
    220         $found   = get_term_by( 'name', 'burrito', 'post_tag' );
    221         $this->assertSame( $term_id, $found->term_id );
    222         $this->assertStringContainsString( 'LIMIT 1', $wpdb->last_query );
     220        add_filter( 'terms_pre_query', array( $this, 'get_query_from_filter' ), 10, 2 );
     221        $found = get_term_by( 'name', 'burrito', 'post_tag' );
     222        $this->assertSame( $term_id, $found->term_id );
     223        $this->assertStringContainsString( 'LIMIT 1', $this->query );
    223224    }
    224225
     
    283284        $this->assertFalse( $found_by_name );
    284285    }
     286
     287    public function get_query_from_filter( $terms, $wp_term_query ) {
     288        $this->query = $wp_term_query->request;
     289
     290        return $terms;
     291    }
    285292}
  • trunk/tests/phpunit/tests/term/getTerms.php

    r52010 r52836  
    119119        $time1 = wp_cache_get( 'last_changed', 'terms' );
    120120        $this->assertNotEmpty( $time1 );
    121         $this->assertSame( $num_queries + 1, $wpdb->num_queries );
     121        $this->assertSame( $num_queries + 2, $wpdb->num_queries );
    122122
    123123        $num_queries = $wpdb->num_queries;
  • trunk/tests/phpunit/tests/term/isObjectInTerm.php

    r46586 r52836  
    146146        $num_queries = $wpdb->num_queries;
    147147        $this->assertTrue( is_object_in_term( $o, 'wptests_tax', $terms[0] ) );
    148         $num_queries++;
     148        $num_queries = $num_queries + 2;
    149149        $this->assertSame( $num_queries, $wpdb->num_queries );
    150150
     
    167167        $num_queries = $wpdb->num_queries;
    168168        $this->assertTrue( is_object_in_term( $o, 'wptests_tax', $terms[0] ) );
    169         $num_queries++;
     169        $num_queries = $num_queries + 2;
    170170        $this->assertSame( $num_queries, $wpdb->num_queries );
    171171
     
    174174        $num_queries = $wpdb->num_queries;
    175175        $this->assertTrue( is_object_in_term( $o, 'wptests_tax', $terms[1] ) );
    176         $num_queries++;
     176        $num_queries = $num_queries + 2;
    177177        $this->assertSame( $num_queries, $wpdb->num_queries );
    178178    }
Note: See TracChangeset for help on using the changeset viewer.