From 545565d593cee1c61150fe3e013e8658cfca745c Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Thu, 4 Nov 2021 11:39:16 +0000 Subject: [PATCH] Fix Duplicate Queries in Product Grids #4695 (#5002) * Cache variation_meta_data to prevent duplicate queries with multiple grids * Prime the cache * Improve existing cache detection * Expand comment --- src/BlockTypes/AbstractProductGrid.php | 80 +++++++++++++++++++++++++- src/StoreApi/Schemas/ProductSchema.php | 63 ++++++++++++++------ 2 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/BlockTypes/AbstractProductGrid.php b/src/BlockTypes/AbstractProductGrid.php index b59e5f6dcb2..aa441c94f5b 100644 --- a/src/BlockTypes/AbstractProductGrid.php +++ b/src/BlockTypes/AbstractProductGrid.php @@ -306,14 +306,92 @@ protected function get_products() { // Remove ordering query arguments which may have been added by get_catalog_ordering_args. WC()->query->remove_ordering_args(); - // Prime caches to reduce future queries. + // Prime caches to reduce future queries. Note _prime_post_caches is private--we could replace this with our own + // query if it becomes unavailable. if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $results ); } + $this->prime_product_variations( $results ); + return $results; } + /** + * Retrieve IDs that are not already present in the cache. + * + * Based on WordPress function: _get_non_cached_ids + * + * @param int[] $product_ids Array of IDs. + * @param string $cache_key The cache bucket to check against. + * @return int[] Array of IDs not present in the cache. + */ + protected function get_non_cached_ids( $product_ids, $cache_key ) { + $non_cached_ids = array(); + $cache_values = wp_cache_get_multiple( $product_ids, $cache_key ); + + foreach ( $cache_values as $id => $value ) { + if ( ! $value ) { + $non_cached_ids[] = (int) $id; + } + } + + return $non_cached_ids; + } + + /** + * Prime query cache of product variation meta data. + * + * Prepares values in the product_ID_variation_meta_data cache for later use in the ProductSchema::get_variations() + * method. Doing so here reduces the total number of queries needed. + * + * @param int[] $product_ids Product ids to prime variation cache for. + */ + protected function prime_product_variations( $product_ids ) { + $cache_group = 'product_variation_meta_data'; + $prime_product_ids = $this->get_non_cached_ids( wp_parse_id_list( $product_ids ), $cache_group ); + + if ( ! $prime_product_ids ) { + return; + } + + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $product_variations = $wpdb->get_results( "SELECT ID as variation_id, post_parent as product_id from {$wpdb->posts} WHERE post_parent IN ( " . implode( ',', $prime_product_ids ) . ' )', ARRAY_A ); + $prime_variation_ids = array_column( $product_variations, 'variation_id' ); + $variation_ids_by_parent = array_column( $product_variations, 'product_id', 'variation_id' ); + $all_variation_meta_data = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $prime_variation_ids ) ) . ') AND meta_key LIKE %s', + $wpdb->esc_like( 'attribute_' ) . '%' + ) + ); + // phpcs:enable + + // Prepare the data to cache by indexing by the parent product. + $primed_data = array_reduce( + $all_variation_meta_data, + function( $values, $data ) use ( $variation_ids_by_parent ) { + $values[ $variation_ids_by_parent[ $data->variation_id ] ?? 0 ][] = $data; + return $values; + }, + array_fill_keys( $prime_product_ids, [] ) + ); + + // Cache everything. + foreach ( $primed_data as $product_id => $variation_meta_data ) { + wp_cache_set( + $product_id, + [ + 'last_modified' => get_the_modified_date( 'U', $product_id ), + 'data' => $variation_meta_data, + ], + $cache_group + ); + } + } + /** * Get the list of classes to apply to this block. * diff --git a/src/StoreApi/Schemas/ProductSchema.php b/src/StoreApi/Schemas/ProductSchema.php index ba431f7ed61..278e6f666ba 100644 --- a/src/StoreApi/Schemas/ProductSchema.php +++ b/src/StoreApi/Schemas/ProductSchema.php @@ -566,17 +566,15 @@ protected function filter_variation_attribute( $attribute ) { * @returns array */ protected function get_variations( \WC_Product $product ) { - if ( ! $product->is_type( 'variable' ) ) { - return []; - } - global $wpdb; - - $variation_ids = $product->get_visible_children(); + $variation_ids = $product->is_type( 'variable' ) ? $product->get_visible_children() : []; if ( ! count( $variation_ids ) ) { return []; } + /** + * Gets default variation data which applies to all of this products variations. + */ $attributes = array_filter( $product->get_attributes(), [ $this, 'filter_variation_attribute' ] ); $default_variation_meta_data = array_reduce( $attributes, @@ -590,22 +588,53 @@ function( $defaults, $attribute ) use ( $product ) { }, [] ); + $default_variation_meta_keys = array_keys( $default_variation_meta_data ); - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $variation_meta_data = $wpdb->get_results( + /** + * Gets individual variation data from the database, using cache where possible. + */ + $cache_group = 'product_variation_meta_data'; + $cache_value = wp_cache_get( $product->get_id(), $cache_group ); + $last_modified = get_the_modified_date( 'U', $product->get_id() ); + + if ( false === $cache_value || $last_modified !== $cache_value['last_modified'] ) { + global $wpdb; + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $variation_meta_data = $wpdb->get_results( + " + SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value + FROM {$wpdb->postmeta} + WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $variation_ids ) ) . ") + AND meta_key IN ('" . implode( "','", array_map( 'esc_sql', $default_variation_meta_keys ) ) . "') " - SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value - FROM {$wpdb->postmeta} - WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $variation_ids ) ) . ") - AND meta_key IN ('" . implode( "','", array_map( 'esc_sql', array_keys( $default_variation_meta_data ) ) ) . "') - " - ); - // phpcs:enable + ); + // phpcs:enable + wp_cache_set( + $product->get_id(), + [ + 'last_modified' => $last_modified, + 'data' => $variation_meta_data, + ], + $cache_group + ); + } else { + $variation_meta_data = $cache_value['data']; + } + + /** + * Merges and formats default variation data with individual variation data. + */ $attributes_by_variation = array_reduce( $variation_meta_data, - function( $values, $data ) { - $values[ $data->variation_id ][ $data->attribute_key ] = $data->attribute_value; + function( $values, $data ) use ( $default_variation_meta_keys ) { + // The query above only includes the keys of $default_variation_meta_data so we know all of the attributes + // being processed here apply to this product. However, we need an additional check here because the + // cache may have been primed elsewhere and include keys from other products. + // @see AbstractProductGrid::prime_product_variations. + if ( in_array( $data->attribute_key, $default_variation_meta_keys, true ) ) { + $values[ $data->variation_id ][ $data->attribute_key ] = $data->attribute_value; + } return $values; }, array_fill_keys( $variation_ids, [] )