Skip to content

Commit

Permalink
Fix Duplicate Queries in Product Grids woocommerce#4695 (woocommerce#…
Browse files Browse the repository at this point in the history
…5002)

* Cache variation_meta_data to prevent duplicate queries with multiple grids

* Prime the cache

* Improve existing cache detection

* Expand comment
  • Loading branch information
mikejolley authored and jonny-bull committed Dec 16, 2021
1 parent 033264b commit 545565d
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 18 deletions.
80 changes: 79 additions & 1 deletion src/BlockTypes/AbstractProductGrid.php
Expand Up @@ -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.
*
Expand Down
63 changes: 46 additions & 17 deletions src/StoreApi/Schemas/ProductSchema.php
Expand Up @@ -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,
Expand All @@ -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, [] )
Expand Down

0 comments on commit 545565d

Please sign in to comment.