Commit 1879d1d18af for woocommerce
commit 1879d1d18af6067bb03fd7d19e0b7552b4a6a2b7
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Fri Mar 27 11:43:50 2026 +0100
[Performance] Non-transient based caching for needs_processing (#63768)
This update reduces the cost of constructing product objects in needs_processing by bulk priming order(s) products, allowing a transition from transients to cache APIs.
diff --git a/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement b/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement
new file mode 100644
index 00000000000..9e8129a8c0a
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Reduced the number of SQL queries required to handle order collections in both the storefront and REST interfaces.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index ab1975c7764..a21c69f667a 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -306,7 +306,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
}
if ( $items_changed ) {
- delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
+ wp_cache_delete( 'order-needs-processing-' . $this->get_id(), 'orders' );
// Invalidate the order cache to prevent stale item data.
// This fixes a race condition where get_items() may have been called
@@ -947,7 +947,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
if ( $group ) {
if ( ! isset( $this->items[ $group ] ) ) {
- $this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) );
+ $read_items = array_filter( $this->data_store->read_items( $this, $type ) );
+
+ // Prime the product cache to ensure that methods such as needs_processing, get_downloadable_items, and has_downloadable_item run
+ // on warm post meta caches for products. This addresses scenarios where the order object was not populated during a batch population.
+ if ( 'line_item' === $type && ! empty( $read_items ) ) {
+ $product_ids = array_map( static fn( $item ) => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), $read_items );
+ $product_ids = array_unique( array_filter( $product_ids ) );
+ _prime_post_caches( $product_ids );
+ }
+
+ $this->items[ $group ] = $read_items;
}
// Don't use array_merge here because keys are numeric.
$items = $items + $this->items[ $group ];
@@ -1660,7 +1670,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$item->save();
$this->add_item( $item );
wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' );
- delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
+ wp_cache_delete( 'order-needs-processing-' . $this->get_id(), 'orders' );
return $item->get_id();
}
diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php
index f93fd6244b8..4a011e4c26e 100644
--- a/plugins/woocommerce/includes/class-wc-order.php
+++ b/plugins/woocommerce/includes/class-wc-order.php
@@ -1862,16 +1862,15 @@ class WC_Order extends WC_Abstract_Order {
* Orders which only contain virtual, downloadable items do not need admin
* intervention.
*
- * Uses a transient so these calls are not repeated multiple times, and because
- * once the order is processed this code/transient does not need to persist.
- *
+ * @since 10.8.0 the method no longer uses transients.
* @since 3.0.0
+ *
* @return bool
*/
public function needs_processing() {
- $transient_name = 'wc_order_' . $this->get_id() . '_needs_processing';
- $needs_processing = get_transient( $transient_name );
-
+ $order_id = $this->get_id();
+ $cache_key = 'order-needs-processing-' . $order_id;
+ $needs_processing = wp_cache_get( $cache_key, 'orders' );
if ( false === $needs_processing ) {
$needs_processing = 0;
@@ -1880,22 +1879,30 @@ class WC_Order extends WC_Abstract_Order {
foreach ( $line_items as $item ) {
if ( $item->is_type( 'line_item' ) ) {
$product = $item->get_product();
-
- if ( ! $product ) {
- continue;
- }
-
- $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();
-
- if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) {
- $needs_processing = 1;
- break;
+ if ( $product ) {
+ $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();
+ /**
+ * Filters whether an order line item requires processing. By default, only
+ * virtual downloadable items do not require processing; all other items do.
+ *
+ * @since 2.7.0
+ *
+ * @param bool $needs_processing
+ * @param \WC_Product $product
+ * @param int $order_id
+ * @return bool
+ */
+ $custom_needs_processing = (bool) apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $order_id );
+ if ( $custom_needs_processing ) {
+ $needs_processing = 1;
+ break;
+ }
}
}
}
}
- set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS );
+ wp_cache_set( $cache_key, $needs_processing, 'orders', DAY_IN_SECONDS );
}
return 1 === absint( $needs_processing );
diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
index 5260753b308..accb74dc100 100644
--- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
@@ -593,12 +593,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
return;
}
}
- $cache_keys = array_map(
- function ( $order_id ) {
- return 'order-items-' . $order_id;
- },
- $order_ids
- );
+ $cache_keys = array_map( static fn( $order_id ) => 'order-items-' . $order_id, $order_ids );
$cache_values = wc_cache_get_multiple( $cache_keys, 'orders' );
$non_cached_ids = array();
foreach ( $order_ids as $order_id ) {
@@ -658,7 +653,35 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
$raw_meta_data_collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
}
\WC_Order_Item::prime_raw_meta_data_cache( $raw_meta_data_collection, 'order-items' );
+
+ $this->prime_product_post_caches_for_order_items( $order_items, $raw_meta_data_collection );
+ }
+ }
+
+ /**
+ * Primes post caches for products which are referenced in line items with 'line_item' type.
+ *
+ * Although the product data store can be replaced, maintaining the posts table connection, as with HPOS, is necessary
+ * for products to function properly. We can therefore prime the post cache directly without compromising store isolation.
+ *
+ * @since 10.8.0
+ *
+ * @param array<int,object{order_item_id:int, order_item_type:string}> $line_items_all Line item entries.
+ * @param array<int,array<int,object{meta_key:string, meta_value:mixed}>> $raw_meta_data_collection Meta-entries grouped by line item id.
+ * @return void
+ */
+ private function prime_product_post_caches_for_order_items( array $line_items_all, array $raw_meta_data_collection ): void {
+ $product_ids = array();
+ foreach ( $line_items_all as $line_item ) {
+ if ( 'line_item' === $line_item->order_item_type ) {
+ foreach ( $raw_meta_data_collection[ $line_item->order_item_id ] ?? array() as $meta ) {
+ if ( ( '_variation_id' === $meta->meta_key || '_product_id' === $meta->meta_key ) && $meta->meta_value > 0 ) {
+ $product_ids[] = (int) $meta->meta_value;
+ }
+ }
+ }
}
+ _prime_post_caches( array_unique( $product_ids ) );
}
/**
@@ -736,19 +759,14 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
* object cache for all transient option names in a single query, we
* eliminate the N+1.
*
+ * @since 10.7.0
+ * @deprecated 10.8.0 `\WC_Order::needs_processing` method no longer uses transients.
+ *
* @param array $order_ids Order IDs to prime cache for.
* @param array $query_vars Query vars for the query.
* @return void
- * @since 10.7.0
*/
protected function prime_needs_processing_transients( $order_ids, $query_vars ) {
- $option_names = array();
- foreach ( $order_ids as $order_id ) {
- $option_names[] = '_transient_wc_order_' . $order_id . '_needs_processing';
- $option_names[] = '_transient_timeout_wc_order_' . $order_id . '_needs_processing';
- }
-
- wp_prime_option_caches( $option_names );
}
/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
index 3060c096854..975d40184f7 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
@@ -1181,7 +1181,6 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
- $this->prime_needs_processing_transients( $order_ids, $query_vars );
}
/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
index 0f41e9ad347..b80c77501e8 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
@@ -184,6 +184,7 @@ class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface {
}
if ( $order_id ) {
wp_cache_delete( 'order-items-' . $order_id, 'orders' );
+ wp_cache_delete( 'order-needs-processing-' . $order_id, 'orders' );
}
}
}
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index 5da09df805d..058d7f0c3cc 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -3150,7 +3150,6 @@ FROM $order_meta_table
$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
- $this->prime_needs_processing_transients( $order_ids, $query_vars );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 2e36b8b9054..4d09ff7d147 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -4110,4 +4110,36 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->assertCount( 1, $fresh_order->get_items(), 'Order should have items from DB.' );
$this->assertTrue( $fresh_order->needs_processing(), 'Order with physical product should need processing.' );
}
+
+ /**
+ * Verifies that needs_processing caching functions as intended.
+ */
+ public function test_needs_processing_caching_correctness(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->save();
+
+ $order = new WC_Order();
+ $item = new WC_Order_Item_Product();
+ $item->set_props(
+ array(
+ 'product' => $product,
+ 'quantity' => 1,
+ 'subtotal' => 10,
+ 'total' => 10,
+ )
+ );
+ $order->add_item( $item );
+ $order->save();
+ $order_id = $order->get_id();
+
+ $this->assertTrue( $order->needs_processing() );
+ $this->assertSame( 1, wp_cache_get( 'order-needs-processing-' . $order_id, 'orders' ) );
+
+ wp_cache_set( 'order-needs-processing-' . $order_id, 0, 'orders' );
+ $this->assertFalse( $order->needs_processing() );
+ $this->assertSame( 0, wp_cache_get( 'order-needs-processing-' . $order_id, 'orders' ) );
+
+ $order->delete();
+ $product->delete();
+ }
}