WooCommerce Data Stores
Data Stores are WooCommerce’s persistence layer—the abstraction that connects high-level CRUD objects (orders, products, customers, coupons, etc.) to a storage backend. Each data object (a subclass of WC_Data
, e.g., WC_Order
, WC_Product
) delegates create/read/update/delete
to its assigned data store.
This design lets WooCommerce (and your plugins) change how data is stored without touching business logic. For example, orders can live in legacy posts/postmeta or in HPOS custom tables; product data continues to use posts, taxonomies, and lookups; customers bridge WordPress users and usermeta. Because code talks to the object API (getters/setters and save()
), it remains stable across schema changes, performance upgrades, and even custom/external databases—so long as the store implements the right interface.
Under the hood, a factory maps object types to concrete store classes (via filters). When you call $order->save()
, WooCommerce picks the correct store (CPT vs HPOS) and performs the write, maintains lookups, and triggers hooks. Queries (wc_get_orders()
, wc_get_products()
) also route through stores so arguments are translated into efficient backend queries.
How it works (high level)
- Objects: Most persisted entities extend
WC_Data
(e.g.,WC_Order
,WC_Product
,WC_Customer
,WC_Coupon
). - Mapping: A registry (filter
woocommerce_data_stores
) maps logical types like'order'
,'product'
,'customer'
to specific store classes. - CRUD flow:
$object->save()
→ store decides create vs update → writes to storage → syncs lookups → fires actions. - Querying: Helpers like
wc_get_orders()
/wc_get_products()
ask the store to build backend-specific queries. - HPOS aware: For orders, the mapping auto-selects legacy CPT or High-Performance Order Storage tables; your code doesn’t change.
Core stores you’ll encounter
- Orders → CPT (legacy) or HPOS custom tables (recommended).
- Products & Variations → posts/postmeta + taxonomies + product lookup tables.
- Coupons → posts/postmeta (
shop_coupon
). - Customers → WordPress users/usermeta (+ customer lookup).
- Tax rates → WooCommerce tax tables.
- Shipping zones & methods → WooCommerce shipping tables.
- Payment tokens → WooCommerce payment token tables.
- Download permissions → WooCommerce downloads table.
Rule of thumb: Use CRUD & helpers, not direct SQL or
update_post_meta()
on core objects—this keeps HPOS and lookups in sync.
Everyday usage (CRUD & queries)
// Create a product (store handles persistence)
$p = new WC_Product_Simple();
$p->set_name('Example Tee');
$p->set_regular_price('19.90');
$p->save(); // store decides create vs update
// Update an order safely (HPOS/CPT agnostic)
$order = wc_get_order( $order_id );
$order->update_meta_data('_gift_note', 'Happy birthday!');
$order->save();
// Query orders (routes through the order store)
$orders = wc_get_orders([
'status' => ['processing', 'completed'],
'billing_email'=> 'user@example.com',
'limit' => 20,
]);
Overriding or adding a store (advanced)
You can swap a store or register your own—for example, to write orders to a proprietary table or external service. Implement the relevant store interface (e.g., order/product/customer) and map it via the registry.
// 1) Map logical types to your store class
add_filter('woocommerce_data_stores', function ($stores) {
// Replace the order store with your implementation
$stores['order'] = \MyPlugin\Stores\My_Order_Store::class;
return $stores;
});
// 2) Implement required CRUD in your store class
namespace MyPlugin\Stores;
use WC_Order;
use WC_Order_Data_Store_Interface;
class My_Order_Store implements WC_Order_Data_Store_Interface {
public function create( &$order ) { /* insert into your tables */ }
public function read( &$order ) { /* hydrate from your tables */ }
public function update( &$order ) { /* update rows */ }
public function delete( &$order, $args = [] ) { /* soft/hard delete */ }
// …implement other required methods/search helpers…
}
Notes:
- If you override orders, ensure compatibility with HPOS features and order lookups/reports.
- For products, consider performance and taxonomy/lookup sync.
- Always run integration tests against both legacy and HPOS environments when relevant.
HPOS co-existence checks (orders)
If you need conditional logic at runtime:
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
// HPOS path
} else {
// Legacy CPT path
}
Best practices
- Stick to object APIs (
get_*/set_*
,save()
,wc_get_*
) to remain storage-agnostic. - Avoid direct SQL/meta writes on core objects; they may bypass lookups and break HPOS.
- Batch carefully: use store-level batch helpers or CLI for large imports.
- Minimize payloads: store big blobs elsewhere; save IDs/refs in object meta.
- Test migrations: if switching stores (e.g., enabling HPOS), verify reads/writes, queries, and reports.
Pitfalls
- Writing straight to
wp_posts/wp_postmeta
for orders → breaks under HPOS. - Querying with raw
WP_Query
for products/orders → may miss store-specific optimizations. - Forgetting to implement all interface methods when creating a custom store.
- Not syncing lookup tables after manual DB changes.