Commit fe30f760f94 for woocommerce
commit fe30f760f94d7e12550ed183734d543c0328e480
Author: Chris Huber <chubes@extrachill.com>
Date: Mon Jun 29 12:42:25 2026 -0400
Performance: skip redundant meta existence checks on product create (#65595)
diff --git a/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path b/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path
new file mode 100644
index 00000000000..aedf64b4ba5
--- /dev/null
+++ b/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Improve product and variation creation performance by avoiding redundant internal meta existence checks.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
index 657d7c2c1d6..cf941854c27 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
@@ -247,24 +247,78 @@ class WC_Data_Store_WP {
*
* Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
*
- * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
+ * @param WC_Data $data_object The WP_Data object (WC_Coupon for coupons, etc).
* @param string $meta_key Meta key to update.
* @param mixed $meta_value Value to save.
- *
+ * @param bool $creating Whether the object is being created.
+ * @param array $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
* @since 3.6.0 Added to prevent empty meta being stored unless required.
*
- * @return bool True if updated/deleted.
+ * @return bool True if added/updated/deleted.
*/
- protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
- if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
- $updated = delete_post_meta( $object->get_id(), $meta_key );
+ protected function update_or_delete_post_meta( $data_object, $meta_key, $meta_value, $creating = false, &$existing_meta_keys = null ) {
+ $is_empty_meta_value = in_array( $meta_value, array( array(), '' ), true );
+ $must_exist = in_array( $meta_key, $this->must_exist_meta_keys, true );
+ $object_id = $data_object->get_id();
+
+ if ( $creating ) {
+ if ( ! is_array( $existing_meta_keys ) ) {
+ $existing_meta_keys = $this->get_existing_meta_keys( $object_id );
+ }
+
+ $meta_exists = isset( $existing_meta_keys[ $meta_key ] );
+
+ if ( $is_empty_meta_value && ! $must_exist ) {
+ if ( ! $meta_exists ) {
+ return false;
+ }
+
+ $updated = delete_post_meta( $object_id, $meta_key );
+ unset( $existing_meta_keys[ $meta_key ] );
+
+ return (bool) $updated;
+ }
+
+ if ( $meta_exists ) {
+ $updated = update_post_meta( $object_id, $meta_key, $meta_value );
+ } else {
+ $updated = add_post_meta( $object_id, $meta_key, $meta_value );
+ if ( $updated ) {
+ $existing_meta_keys[ $meta_key ] = true;
+ }
+ }
+
+ return (bool) $updated;
+ }
+
+ if ( $is_empty_meta_value && ! $must_exist ) {
+ $updated = delete_post_meta( $object_id, $meta_key );
} else {
- $updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
+ $updated = update_post_meta( $object_id, $meta_key, $meta_value );
}
return (bool) $updated;
}
+ /**
+ * Get the set of meta keys an object already has, to avoid per-key existence queries during creation.
+ *
+ * @param int $object_id Object ID.
+ * @return array Map of existing meta key => true.
+ */
+ protected function get_existing_meta_keys( $object_id ) {
+ global $wpdb;
+
+ $meta_keys = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT meta_key FROM {$wpdb->postmeta} WHERE post_id = %d",
+ $object_id
+ )
+ );
+
+ return array_fill_keys( $meta_keys, true );
+ }
+
/**
* Get valid WP_Query args from a WC_Object_Query's query variables.
*
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index de9b861600d..59798c645e5 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -248,7 +248,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$post_object = get_post( $product->get_id() );
$product->set_status( $post_object->post_status );
- $this->update_post_meta( $product, true );
+ $this->update_post_meta_internal( $product, true, true );
$this->update_terms( $product, true );
$this->update_visibility( $product, true );
$this->update_attributes( $product, true );
@@ -697,12 +697,31 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
/**
* Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
*
+ * Subclasses that override this method to write custom product-type meta should also override
+ * update_post_meta_internal() so those writes run during object creation.
+ *
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
* @since 3.0.0
* @return void
*/
protected function update_post_meta( &$product, $force = false ) {
+ $this->update_post_meta_internal( $product, $force, false );
+ }
+
+ /**
+ * Internal implementation of update_post_meta() that also knows whether the product is being created.
+ *
+ * Subclasses that override update_post_meta() to write custom product-type meta should also override
+ * this method so those writes run during object creation.
+ *
+ * @param WC_Product $product Product object.
+ * @param bool $force Force update. Used during create.
+ * @param bool $creating Whether the product is being created.
+ * @param array $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
+ * @return void
+ */
+ protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
$meta_key_to_props = array(
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
@@ -788,7 +807,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
break;
}
- $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
+ $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
if ( $updated ) {
$this->updated_props[] = $prop;
@@ -810,7 +829,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$cogs_value = apply_filters( 'woocommerce_save_product_cogs_value', $cogs_value, $product );
if ( false !== $cogs_value ) {
- $updated = $this->update_or_delete_post_meta( $product, '_cogs_total_value', is_null( $cogs_value ) ? '' : $cogs_value );
+ $updated = $this->update_or_delete_post_meta( $product, '_cogs_total_value', is_null( $cogs_value ) ? '' : $cogs_value, $creating, $existing_meta_keys );
if ( $updated ) {
$this->updated_props[] = 'cogs_value';
}
@@ -828,7 +847,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
if ( is_callable( array( $product, $function ) ) ) {
$value = $product->{$function}( 'edit' );
$value = is_string( $value ) ? wp_slash( $value ) : $value;
- $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
+ $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
if ( $updated ) {
$this->updated_props[] = $key;
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
index f21b9e6fa74..53188472c12 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
@@ -24,6 +24,19 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem
* @since 3.0.0
*/
protected function update_post_meta( &$product, $force = false ) {
+ $this->update_post_meta_internal( $product, $force, false );
+ }
+
+ /**
+ * Internal implementation of update_post_meta() that also knows whether the product is being created.
+ *
+ * @param WC_Product $product Product object.
+ * @param bool $force Force update. Used during create.
+ * @param bool $creating Whether the product is being created.
+ * @param array $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
+ * @return void
+ */
+ protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
$meta_key_to_props = array(
'_children' => 'children',
);
@@ -38,7 +51,7 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem
}
}
- parent::update_post_meta( $product, $force );
+ parent::update_post_meta_internal( $product, $force, $creating, $existing_meta_keys );
}
/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
index 731f2d0a242..6df569ac85a 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
@@ -20,6 +20,18 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 3.0.0
*/
class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {
+ /**
+ * Meta data which should exist in the DB, even if empty.
+ *
+ * @since 3.6.0
+ *
+ * @var array
+ */
+ protected $must_exist_meta_keys = array(
+ '_tax_class',
+ '_variation_description',
+ );
+
/**
* Callback to remove unwanted meta data.
*
@@ -157,7 +169,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
if ( $id && ! is_wp_error( $id ) ) {
$product->set_id( $id );
- $this->update_post_meta( $product, true );
+ $this->update_post_meta_internal( $product, true, true );
$this->update_terms( $product, true );
$this->update_visibility( $product, true );
$this->update_attributes( $product, true );
@@ -595,6 +607,19 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
* @param bool $force Force update. Used during create.
*/
public function update_post_meta( &$product, $force = false ) {
+ $this->update_post_meta_internal( $product, $force, false );
+ }
+
+ /**
+ * Internal implementation of update_post_meta() that also knows whether the variation is being created.
+ *
+ * @since 3.0.0
+ * @param WC_Product $product Product object.
+ * @param bool $force Force update. Used during create.
+ * @param bool $creating Whether the variation is being created.
+ * @param array $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
+ */
+ protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
$meta_key_to_props = array(
'_variation_description' => 'description',
);
@@ -603,7 +628,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $product->{"get_$prop"}( 'edit' );
- $updated = update_post_meta( $product->get_id(), $meta_key, $value );
+ $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
if ( $updated ) {
$this->updated_props[] = $prop;
}
@@ -625,14 +650,14 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
$cogs_value_is_additive = apply_filters( 'woocommerce_save_product_cogs_is_additive_flag', $cogs_value_is_additive, $product );
if ( ! is_null( $cogs_value_is_additive ) ) {
- $updated = $this->update_or_delete_post_meta( $product, '_cogs_value_is_additive', $cogs_value_is_additive ? 'yes' : '' );
+ $updated = $this->update_or_delete_post_meta( $product, '_cogs_value_is_additive', $cogs_value_is_additive ? 'yes' : '', $creating, $existing_meta_keys );
if ( $updated ) {
$this->updated_props[] = 'cogs_value_is_additive';
}
}
}
- parent::update_post_meta( $product, $force );
+ parent::update_post_meta_internal( $product, $force, $creating, $existing_meta_keys );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
index 27c688253fa..33a2a9e800d 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
@@ -362,6 +362,57 @@ class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertSame( '30.000000', get_post_meta( $product_id, '_stock', true ) );
}
+ /**
+ * @testdox Creating a product updates internal meta pre-created by save_post hooks instead of duplicating it.
+ */
+ public function test_create_updates_internal_meta_precreated_by_save_post_hooks(): void {
+ $precreate_stock_meta = function ( $post_id, $post, $update ) {
+ if ( $update || metadata_exists( 'post', $post_id, '_stock' ) ) {
+ return;
+ }
+
+ add_post_meta( $post_id, '_manage_stock', 'no' );
+ add_post_meta( $post_id, '_stock', '0' );
+ };
+
+ add_action( 'save_post_product', $precreate_stock_meta, 10, 3 );
+
+ try {
+ $product = new WC_Product();
+ $product->set_name( 'Pre-created meta product' );
+ $product->set_manage_stock( true );
+ $product->set_stock_quantity( 5 );
+ $product->save();
+ } finally {
+ remove_action( 'save_post_product', $precreate_stock_meta, 10 );
+ }
+
+ $product_id = $product->get_id();
+
+ $this->assertSame( array( 'yes' ), get_post_meta( $product_id, '_manage_stock', false ) );
+ $this->assertSame( array( '5' ), get_post_meta( $product_id, '_stock', false ) );
+ $this->assertTrue( wc_get_product( $product_id )->get_manage_stock() );
+ $this->assertSame( 5, wc_get_product( $product_id )->get_stock_quantity() );
+ }
+
+ /**
+ * @testdox Creating a grouped product persists children on the initial save.
+ */
+ public function test_grouped_product_create_persists_children_on_initial_save(): void {
+ $child_1 = WC_Helper_Product::create_simple_product();
+ $child_2 = WC_Helper_Product::create_simple_product();
+
+ $product = new WC_Product_Grouped();
+ $product->set_name( 'Grouped product' );
+ $product->set_children( array( $child_1->get_id(), $child_2->get_id() ) );
+ $product->save();
+
+ $product_id = $product->get_id();
+
+ $this->assertSame( array( $child_1->get_id(), $child_2->get_id() ), get_post_meta( $product_id, '_children', true ) );
+ $this->assertSame( array( $child_1->get_id(), $child_2->get_id() ), wc_get_product( $product_id )->get_children() );
+ }
+
/**
* Test update_product_sales updates on the meta-entry.
*/
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
index d1fdb94a2dc..b61bca2786a 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
@@ -295,6 +295,73 @@ class WC_Product_Variation_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertEquals( $flag_value ? 'yes' : '', get_post_meta( $product->get_id(), '_cogs_value_is_additive', true ) );
}
+ /**
+ * @testdox Creating a variation inserts known-new internal meta without update/delete hooks.
+ */
+ public function test_create_inserts_known_new_internal_meta_without_update_or_delete_hooks() {
+ $parent = new WC_Product_Variable();
+ $parent->set_name( 'Variable product' );
+ $parent_id = $parent->save();
+
+ $added_meta_events = array();
+ $updated_meta_events = array();
+ $deleted_meta_events = array();
+
+ $added_meta_observer = function ( $meta_id, $object_id, $meta_key ) use ( &$added_meta_events ) {
+ $added_meta_events[] = array( $object_id, $meta_key );
+ };
+
+ $updated_meta_observer = function ( $meta_id, $object_id, $meta_key ) use ( &$updated_meta_events ) {
+ $updated_meta_events[] = array( $object_id, $meta_key );
+ };
+
+ $deleted_meta_observer = function ( $meta_ids, $object_id, $meta_key ) use ( &$deleted_meta_events ) {
+ $deleted_meta_events[] = array( $object_id, $meta_key );
+ };
+
+ add_action( 'added_post_meta', $added_meta_observer, 10, 3 );
+ add_action( 'updated_post_meta', $updated_meta_observer, 10, 3 );
+ add_action( 'deleted_post_meta', $deleted_meta_observer, 10, 3 );
+
+ try {
+ $variation = new WC_Product_Variation();
+ $variation->set_parent_id( $parent_id );
+ $variation->set_sku( 'test-variation-fast-path' );
+ $variation->set_regular_price( '12.50' );
+ $variation->set_manage_stock( true );
+ $variation->set_stock_quantity( 4 );
+ $variation->save();
+ } finally {
+ remove_action( 'added_post_meta', $added_meta_observer, 10 );
+ remove_action( 'updated_post_meta', $updated_meta_observer, 10 );
+ remove_action( 'deleted_post_meta', $deleted_meta_observer, 10 );
+ }
+
+ $variation_id = $variation->get_id();
+ $filter_to_variation_id = static function ( array $events ) use ( $variation_id ) {
+ return array_values(
+ array_map(
+ static fn( $event ) => $event[1],
+ array_filter( $events, static fn( $event ) => (int) $event[0] === $variation_id )
+ )
+ );
+ };
+
+ $added_meta_keys = $filter_to_variation_id( $added_meta_events );
+ $updated_meta_keys = $filter_to_variation_id( $updated_meta_events );
+ $deleted_meta_keys = $filter_to_variation_id( $deleted_meta_events );
+
+ $this->assertContains( '_sku', $added_meta_keys );
+ $this->assertContains( '_regular_price', $added_meta_keys );
+ $this->assertContains( '_price', $added_meta_keys );
+ $this->assertContains( '_manage_stock', $added_meta_keys );
+ $this->assertContains( '_stock', $added_meta_keys );
+ $this->assertContains( '_variation_description', $added_meta_keys );
+ $this->assertEmpty( $updated_meta_keys );
+ $this->assertEmpty( $deleted_meta_keys );
+ $this->assertSame( array( '' ), get_post_meta( $variation_id, '_variation_description', false ) );
+ }
+
/**
* Create a variable product and return one of its variations.
*