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.