WordPress.org

Make WordPress Core

Changeset 34529


Ignore:
Timestamp:
09/25/2015 03:58:59 AM (4 years ago)
Author:
boonebgorges
Message:

Introduce metadata for taxonomy terms.

Adds a new table to the database schema (wp_termmeta), and a set of
*_term_meta() API functions. get_terms() and wp_get_object_terms()
now also support 'meta_query' parameters, with syntax identical to other
uses of WP_Meta_Query.

When fetching terms via get_terms() or wp_get_object_terms(), metadata for
matched terms is preloaded into the cache by default. Disable this behavior
by setting the new $update_term_meta_cache paramater to false.

To maximize performance, within WP_Query loops, the termmeta cache is *not*
primed by default. Instead, we use a lazy-loading technique: metadata for all
terms belonging to posts in the loop is loaded into the cache the first time
that get_term_meta() is called within the loop.

Props boonebgorges, sirzooro.
See #10142.

Location:
trunk
Files:
1 added
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/schema.php

    r34298 r34529  
    5858
    5959    // Blog specific tables.
    60     $blog_tables = "CREATE TABLE $wpdb->terms (
     60    $blog_tables = "CREATE TABLE $wpdb->termmeta (
     61  meta_id bigint(20) unsigned NOT NULL auto_increment,
     62  term_id bigint(20) unsigned NOT NULL default '0',
     63  meta_key varchar(255) default NULL,
     64  meta_value longtext,
     65  PRIMARY KEY (meta_id),
     66  KEY term_id (term_id),
     67  KEY meta_key (meta_key($max_index_length))
     68) $charset_collate;
     69CREATE TABLE $wpdb->terms (
    6170 term_id bigint(20) unsigned NOT NULL auto_increment,
    6271 name varchar(200) NOT NULL default '',
  • trunk/src/wp-admin/includes/upgrade.php

    r34030 r34529  
    26292629        $wpdb->query( "ALTER TABLE $wpdb->posts DROP INDEX post_name, ADD INDEX post_name(post_name(191))" );
    26302630    }
     2631
     2632    // Upgrade versions prior to 4.4.
     2633    if ( $wp_current_db_version < 34370 ) {
     2634        // If compatible termmeta table is found, use it, but enforce a proper index.
     2635        if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->termmeta}'" ) && $wpdb->get_results( "SHOW INDEX FROM {$wpdb->termmeta} WHERE Column_name = 'meta_key'" ) ) {
     2636            $wpdb->query( "ALTER TABLE $wpdb->termmeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
     2637        }
     2638    }
    26312639}
    26322640
  • trunk/src/wp-includes/default-filters.php

    r34270 r34529  
    202202add_filter( 'title_save_pre',           'trim'                                );
    203203add_filter( 'get_comment_metadata',     'wp_lazyload_comment_meta',     10, 2 );
     204add_filter( 'get_term_metadata',        'wp_lazyload_term_meta',        10, 2 );
    204205
    205206add_filter( 'http_request_host_is_external', 'allowed_http_request_hosts', 10, 2 );
  • trunk/src/wp-includes/taxonomy-functions.php

    r34516 r34529  
    958958 * @since 4.2.0 Introduced 'name' and 'childless' parameters.
    959959 * @since 4.4.0 Introduced the ability to pass 'term_id' as an alias of 'id' for the `orderby` parameter.
     960 *              Introduced the 'meta_query' and 'update_term_meta_cache' parameters.
    960961 *
    961962 * @global wpdb  $wpdb WordPress database abstraction object.
     
    10141015 *     @type string       $cache_domain      Unique cache key to be produced when this query is stored in an
    10151016 *                                           object cache. Default is 'core'.
     1017 *     @type bool         $update_term_meta_cache Whether to prime meta caches for matched terms. Default true.
     1018 *     @type array        $meta_query             Meta query clauses to limit retrieved terms by.
     1019 *                                                See `WP_Meta_Query`. Default empty.
    10161020 * }
    10171021 * @return array|int|WP_Error List of Term Objects and their children. Will return WP_Error, if any of $taxonomies
     
    10371041        'number' => '', 'fields' => 'all', 'name' => '', 'slug' => '', 'parent' => '', 'childless' => false,
    10381042        'hierarchical' => true, 'child_of' => 0, 'get' => '', 'name__like' => '', 'description__like' => '',
    1039         'pad_counts' => false, 'offset' => '', 'search' => '', 'cache_domain' => 'core' );
     1043        'pad_counts' => false, 'offset' => '', 'search' => '', 'cache_domain' => 'core',
     1044        'update_term_meta_cache' => true, 'meta_query' => '' );
    10401045    $args = wp_parse_args( $args, $defaults );
    10411046    $args['number'] = absint( $args['number'] );
     
    12951300        $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
    12961301        $where .= $wpdb->prepare( ' AND ((t.name LIKE %s) OR (t.slug LIKE %s))', $like, $like );
     1302    }
     1303
     1304    // Meta query support.
     1305    $join = '';
     1306    if ( ! empty( $args['meta_query'] ) ) {
     1307        $mquery = new WP_Meta_Query( $args['meta_query'] );
     1308        $mq_sql = $mquery->get_sql( 'term', 't', 'term_id' );
     1309
     1310        $join  .= $mq_sql['join'];
     1311        $where .= $mq_sql['where'];
    12971312    }
    12981313
     
    13421357    $fields = implode( ', ', apply_filters( 'get_terms_fields', $selects, $args, $taxonomies ) );
    13431358
    1344     $join = "INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
     1359    $join .= " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
    13451360
    13461361    $pieces = array( 'fields', 'join', 'where', 'orderby', 'order', 'limits' );
     
    13731388    if ( 'all' == $_fields ) {
    13741389        update_term_cache( $terms );
     1390    }
     1391
     1392    // Prime termmeta cache.
     1393    if ( $args['update_term_meta_cache'] ) {
     1394        $term_ids = wp_list_pluck( $terms, 'term_id' );
     1395        update_termmeta_cache( $term_ids );
    13751396    }
    13761397
     
    14561477
    14571478/**
     1479 * Adds metadata to a term.
     1480 *
     1481 * @since 4.4.0
     1482 *
     1483 * @param int    $term_id    Term ID.
     1484 * @param string $meta_key   Metadata name.
     1485 * @param mixed  $meta_value Metadata value.
     1486 * @param bool   $unique     Optional. Whether to bail if an entry with the same key is found for the term.
     1487 *                           Default false.
     1488 * @return int|bool Meta ID on success, false on failure.
     1489 */
     1490function add_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) {
     1491    return add_metadata( 'term', $term_id, $meta_key, $meta_value, $unique );
     1492}
     1493
     1494/**
     1495 * Removes metadata matching criteria from a term.
     1496 *
     1497 * @since 4.4.0
     1498 *
     1499 * @param int    $term_id    Term ID.
     1500 * @param string $meta_key   Metadata name.
     1501 * @param mixed  $meta_value Optional. Metadata value. If provided, rows will only be removed that match the value.
     1502 * @return bool True on success, false on failure.
     1503 */
     1504function delete_term_meta( $term_id, $meta_key, $meta_value = '' ) {
     1505    return delete_metadata( 'term', $term_id, $meta_key, $meta_value );
     1506}
     1507
     1508/**
     1509 * Retrieves metadata for a term.
     1510 *
     1511 * @since 4.4.0
     1512 *
     1513 * @param int    $term_id Term ID.
     1514 * @param string $key     Optional. The meta key to retrieve. If no key is provided, fetches all metadata for the term.
     1515 * @param bool   $single  Whether to return a single value. If false, an array of all values matching the
     1516 *                        `$term_id`/`$key` pair will be returned. Default: false.
     1517 * @return mixed If `$single` is false, an array of metadata values. If `$single` is true, a single metadata value.
     1518 */
     1519function get_term_meta( $term_id, $key = '', $single = false ) {
     1520    return get_metadata( 'term', $term_id, $key, $single );
     1521}
     1522
     1523/**
     1524 * Updates term metadata.
     1525 *
     1526 * Use the `$prev_value` parameter to differentiate between meta fields with the same key and term ID.
     1527 *
     1528 * If the meta field for the term does not exist, it will be added.
     1529 *
     1530 * @since 4.4.0
     1531 *
     1532 * @param int    $term_id    Term ID.
     1533 * @param string $meta_key   Metadata key.
     1534 * @param mixed  $meta_value Metadata value.
     1535 * @param mixed  $prev_value Optional. Previous value to check before removing.
     1536 * @return int|bool Meta ID if the key didn't previously exist. True on successful update. False on failure.
     1537 */
     1538function update_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) {
     1539    return update_metadata( 'term', $term_id, $meta_key, $meta_value, $prev_value );
     1540}
     1541
     1542/**
     1543 * Updates metadata cache for list of term IDs.
     1544 *
     1545 * Performs SQL query to retrieve all metadata for the terms matching `$term_ids` and stores them in the cache.
     1546 * Subsequent calls to `get_term_meta()` will not need to query the database.
     1547 *
     1548 * @since 4.4.0
     1549 *
     1550 * @param array $term_ids List of term IDs.
     1551 * @return array|false Returns false if there is nothing to update. Returns an array of metadata on success.
     1552 */
     1553function update_termmeta_cache( $term_ids ) {
     1554    return update_meta_cache( 'term', $term_ids );
     1555}
     1556
     1557/**
     1558 * Lazy-loads termmeta when inside of a `WP_Query` loop.
     1559 *
     1560 * As a rule, term queries (`get_terms()` and `wp_get_object_terms()`) prime the metadata cache for matched terms by
     1561 * default. However, this can cause a slight performance penalty, especially when that metadata is not actually used.
     1562 * In the context of a `WP_Query` loop, we're able to avoid this potential penalty. `update_object_term_cache()`,
     1563 * called from `update_post_caches()`, does not 'update_term_meta_cache'. Instead, the first time `get_term_meta()` is
     1564 * called from within a `WP_Query` loop, the current function detects the fact, and then primes the metadata cache for
     1565 * all terms attached to all posts in the loop, with a single database query.
     1566 *
     1567 * @since 4.4.0
     1568 *
     1569 * @param null $check   The `$check` param passed from the 'pre_term_metadata' hook.
     1570 * @param int  $term_id ID of the term whose metadata is being cached.
     1571 * @return null In order not to short-circuit `get_metadata()`.
     1572 */
     1573function wp_lazyload_term_meta( $check, $term_id ) {
     1574    global $wp_query;
     1575
     1576    if ( $wp_query instanceof WP_Query && ! empty( $wp_query->posts ) && $wp_query->get( 'update_post_term_cache' ) ) {
     1577        // We can only lazyload if the entire post object is present.
     1578        $posts = array();
     1579        foreach ( $wp_query->posts as $post ) {
     1580            if ( $post instanceof WP_Post ) {
     1581                $posts[] = $post;
     1582            }
     1583        }
     1584
     1585        if ( empty( $posts ) ) {
     1586            return;
     1587        }
     1588
     1589        // Fetch cached term_ids for each post. Keyed by term_id for faster lookup.
     1590        $term_ids = array();
     1591        foreach ( $posts as $post ) {
     1592            $taxonomies = get_object_taxonomies( $post->post_type );
     1593            foreach ( $taxonomies as $taxonomy ) {
     1594                // No extra queries. Term cache should already be primed by 'update_post_term_cache'.
     1595                $terms = get_object_term_cache( $post->ID, $taxonomy );
     1596                if ( false !== $terms ) {
     1597                    foreach ( $terms as $term ) {
     1598                        if ( ! isset( $term_ids[ $term->term_id ] ) ) {
     1599                            $term_ids[ $term->term_id ] = 1;
     1600                        }
     1601                    }
     1602                }
     1603            }
     1604        }
     1605
     1606        if ( $term_ids ) {
     1607            update_termmeta_cache( array_keys( $term_ids ) );
     1608        }
     1609    }
     1610
     1611    return $check;
     1612}
     1613
     1614/**
    14581615 * Check if Term exists.
    14591616 *
     
    20122169 * @since 4.2.0 Added support for 'taxonomy', 'parent', and 'term_taxonomy_id' values of `$orderby`.
    20132170 *              Introduced `$parent` argument.
     2171 * @since 4.4.0 Introduced `$meta_query` and `$update_term_meta_cache` arguments.
    20142172 *
    20152173 * @global wpdb $wpdb WordPress database abstraction object.
     
    20272185 *                           of strings.
    20282186 *     @type int    $parent  Optional. Limit results to the direct children of a given term ID.
     2187 *     @type bool   $update_term_meta_cache Whether to prime termmeta cache for matched terms. Only applies when
     2188 *                                          `$fields` is 'all', 'all_with_object_id', or 'term_id'. Default true.
     2189 *     @type array  $meta_query             Meta query clauses to limit retrieved terms by. See `WP_Meta_Query`.
     2190 *                                          Default empty.
    20292191 * }
    20302192 * @return array|WP_Error The requested term data or empty array if no terms found.
     
    20542216        'fields'  => 'all',
    20552217        'parent'  => '',
     2218        'update_term_meta_cache' => true,
     2219        'meta_query' => '',
    20562220    );
    20572221    $args = wp_parse_args( $args, $defaults );
     
    21272291    }
    21282292
     2293    // Meta query support.
     2294    $meta_query_join = '';
     2295    if ( ! empty( $args['meta_query'] ) ) {
     2296        $mquery = new WP_Meta_Query( $args['meta_query'] );
     2297        $mq_sql = $mquery->get_sql( 'term', 't', 'term_id' );
     2298
     2299        $meta_query_join .= $mq_sql['join'];
     2300
     2301        // Strip leading AND.
     2302        $where[] = preg_replace( '/^\s*AND/', '', $mq_sql['where'] );
     2303    }
     2304
    21292305    $where = implode( ' AND ', $where );
    21302306
    2131     $query = "SELECT $select_this FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN $wpdb->term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE $where $orderby $order";
     2307    $query = "SELECT $select_this FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN $wpdb->term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id $meta_query_join WHERE $where $orderby $order";
    21322308
    21332309    $objects = false;
     
    21522328            $terms[$key] = sanitize_term_field( 'term_taxonomy_id', $tt_id, 0, $taxonomy, 'raw' ); // 0 should be the term id, however is not needed when using raw context.
    21532329        }
     2330    }
     2331
     2332    // Update termmeta cache, if necessary.
     2333    if ( $args['update_term_meta_cache'] && ( 'all' === $fields || 'all_with_object_ids' === $fields || 'term_id' === $fields ) ) {
     2334        if ( 'term_id' === $fields ) {
     2335            $term_ids = $fields;
     2336        } else {
     2337            $term_ids = wp_list_pluck( $terms, 'term_id' );
     2338        }
     2339
     2340        update_termmeta_cache( $term_ids );
    21542341    }
    21552342
     
    32893476        'fields' => 'all_with_object_id',
    32903477        'orderby' => 'none',
     3478        'update_term_meta_cache' => false,
    32913479    ) );
    32923480
  • trunk/src/wp-includes/version.php

    r34030 r34529  
    1212 * @global int $wp_db_version
    1313 */
    14 $wp_db_version = 34030;
     14$wp_db_version = 34528;
    1515
    1616/**
  • trunk/src/wp-includes/wp-db.php

    r34478 r34529  
    267267     */
    268268    var $tables = array( 'posts', 'comments', 'links', 'options', 'postmeta',
    269         'terms', 'term_taxonomy', 'term_relationships', 'commentmeta' );
     269        'terms', 'term_taxonomy', 'term_relationships', 'termmeta', 'commentmeta' );
    270270
    271271    /**
     
    382382     */
    383383    public $term_taxonomy;
     384
     385    /**
     386     * WordPress Term Meta table.
     387     *
     388     * @since 4.4.0
     389     * @access public
     390     * @var string
     391     */
     392    public $termmeta;
    384393
    385394    /*
  • trunk/tests/phpunit/tests/term/getTerms.php

    r33903 r34529  
    2323
    2424        // last_changed and num_queries should bump
    25         $terms = get_terms( 'post_tag' );
     25        $terms = get_terms( 'post_tag', array( 'update_term_meta_cache' => false ) );
    2626        $this->assertEquals( 3, count( $terms ) );
    2727        $time1 = wp_cache_get( 'last_changed', 'terms' );
     
    3232
    3333        // Again. last_changed and num_queries should remain the same.
    34         $terms = get_terms( 'post_tag' );
     34        $terms = get_terms( 'post_tag', array( 'update_term_meta_cache' => false ) );
    3535        $this->assertEquals( 3, count( $terms ) );
    3636        $this->assertEquals( $time1, wp_cache_get( 'last_changed', 'terms' ) );
     
    15031503    }
    15041504
     1505    /**
     1506     * @ticket 10142
     1507     */
     1508    public function test_termmeta_cache_should_be_primed_by_default() {
     1509        global $wpdb;
     1510
     1511        register_taxonomy( 'wptests_tax', 'post' );
     1512        $terms = $this->factory->term->create_many( 3, array( 'taxonomy' => 'wptests_tax' ) );
     1513        add_term_meta( $terms[0], 'foo', 'bar' );
     1514        add_term_meta( $terms[1], 'foo', 'bar' );
     1515        add_term_meta( $terms[2], 'foo', 'bar' );
     1516
     1517        $found = get_terms( 'wptests_tax', array(
     1518            'hide_empty' => false,
     1519            'include' => $terms,
     1520        ) );
     1521
     1522        $num_queries = $wpdb->num_queries;
     1523
     1524        foreach ( $terms as $t ) {
     1525            $this->assertSame( 'bar', get_term_meta( $t, 'foo', true ) );
     1526        }
     1527
     1528        $this->assertSame( $num_queries, $wpdb->num_queries );
     1529    }
     1530
     1531    /**
     1532     * @ticket 10142
     1533     */
     1534    public function test_termmeta_cache_should_not_be_primed_when_update_term_meta_cache_is_false() {
     1535        global $wpdb;
     1536
     1537        register_taxonomy( 'wptests_tax', 'post' );
     1538        $terms = $this->factory->term->create_many( 3, array( 'taxonomy' => 'wptests_tax' ) );
     1539        add_term_meta( $terms[0], 'foo', 'bar' );
     1540        add_term_meta( $terms[1], 'foo', 'bar' );
     1541        add_term_meta( $terms[2], 'foo', 'bar' );
     1542
     1543        $found = get_terms( 'wptests_tax', array(
     1544            'hide_empty' => false,
     1545            'include' => $terms,
     1546            'update_term_meta_cache' => false,
     1547        ) );
     1548
     1549        $num_queries = $wpdb->num_queries;
     1550
     1551        foreach ( $terms as $t ) {
     1552            $this->assertSame( 'bar', get_term_meta( $t, 'foo', true ) );
     1553        }
     1554
     1555        $this->assertSame( $num_queries + 3, $wpdb->num_queries );
     1556    }
     1557
     1558    /**
     1559     * @ticket 10142
     1560     */
     1561    public function test_meta_query() {
     1562        register_taxonomy( 'wptests_tax', 'post' );
     1563        $terms = $this->factory->term->create_many( 5, array( 'taxonomy' => 'wptests_tax' ) );
     1564        add_term_meta( $terms[0], 'foo', 'bar' );
     1565        add_term_meta( $terms[1], 'foo', 'bar' );
     1566        add_term_meta( $terms[2], 'foo', 'baz' );
     1567        add_term_meta( $terms[3], 'foob', 'ar' );
     1568
     1569        $found = get_terms( 'wptests_tax', array(
     1570            'hide_empty' => false,
     1571            'meta_query' => array(
     1572                array(
     1573                    'key' => 'foo',
     1574                    'value' => 'bar',
     1575                ),
     1576            ),
     1577            'fields' => 'ids',
     1578        ) );
     1579
     1580        $this->assertEqualSets( array( $terms[0], $terms[1] ), $found );
     1581    }
     1582
    15051583    protected function create_hierarchical_terms_and_posts() {
    15061584        $terms = array();
  • trunk/tests/phpunit/tests/term/wpGetObjectTerms.php

    r31270 r34529  
    419419    }
    420420
     421    /**
     422     * @ticket 10142
     423     */
     424    public function test_termmeta_cache_should_be_primed_by_default() {
     425        global $wpdb;
     426
     427        register_taxonomy( 'wptests_tax', 'post' );
     428        $terms = $this->factory->term->create_many( 3, array( 'taxonomy' => 'wptests_tax' ) );
     429        add_term_meta( $terms[0], 'foo', 'bar' );
     430        add_term_meta( $terms[1], 'foo', 'bar' );
     431        add_term_meta( $terms[2], 'foo', 'bar' );
     432
     433        $p = $this->factory->post->create();
     434        wp_set_object_terms( $p, $terms, 'wptests_tax' );
     435
     436        $found = wp_get_object_terms( $p, 'wptests_tax' );
     437
     438        $num_queries = $wpdb->num_queries;
     439
     440        foreach ( $terms as $t ) {
     441            $this->assertSame( 'bar', get_term_meta( $t, 'foo', true ) );
     442        }
     443
     444        $this->assertSame( $num_queries, $wpdb->num_queries );
     445    }
     446
     447    /**
     448     * @ticket 10142
     449     */
     450    public function test_termmeta_cache_should_not_be_primed_when_update_term_meta_cache_is_false() {
     451        global $wpdb;
     452
     453        register_taxonomy( 'wptests_tax', 'post' );
     454        $terms = $this->factory->term->create_many( 3, array( 'taxonomy' => 'wptests_tax' ) );
     455        add_term_meta( $terms[0], 'foo', 'bar' );
     456        add_term_meta( $terms[1], 'foo', 'bar' );
     457        add_term_meta( $terms[2], 'foo', 'bar' );
     458
     459        $p = $this->factory->post->create();
     460        wp_set_object_terms( $p, $terms, 'wptests_tax' );
     461
     462        $found = wp_get_object_terms( $p, 'wptests_tax', array(
     463            'update_term_meta_cache' => false,
     464        ) );
     465
     466        $num_queries = $wpdb->num_queries;
     467
     468        foreach ( $terms as $t ) {
     469            $this->assertSame( 'bar', get_term_meta( $t, 'foo', true ) );
     470        }
     471
     472        $this->assertSame( $num_queries + 3, $wpdb->num_queries );
     473    }
     474
     475    /**
     476     * @ticket 10142
     477     */
     478    public function test_meta_query() {
     479        register_taxonomy( 'wptests_tax', 'post' );
     480        $terms = $this->factory->term->create_many( 5, array( 'taxonomy' => 'wptests_tax' ) );
     481        add_term_meta( $terms[0], 'foo', 'bar' );
     482        add_term_meta( $terms[1], 'foo', 'bar' );
     483        add_term_meta( $terms[2], 'foo', 'baz' );
     484        add_term_meta( $terms[3], 'foob', 'ar' );
     485
     486        $p = $this->factory->post->create();
     487        wp_set_object_terms( $p, $terms, 'wptests_tax' );
     488
     489        $found = wp_get_object_terms( $p, 'wptests_tax', array(
     490            'meta_query' => array(
     491                array(
     492                    'key' => 'foo',
     493                    'value' => 'bar',
     494                ),
     495            ),
     496        ) );
     497
     498        $this->assertEqualSets( array( $terms[0], $terms[1] ), wp_list_pluck( $found, 'term_id' ) );
     499    }
     500
    421501    public function filter_get_object_terms( $terms ) {
    422502        $term_ids = wp_list_pluck( $terms, 'term_id' );
Note: See TracChangeset for help on using the changeset viewer.