Use WC lookup tables for improved performance

This commit is part of a task to allow multiple values in "is in country" segment. This commit replaces the queries with the more performant ones written by @lysyjan. It also creates the lookup tables on the integration database and updates the test.

[MAILPOET-3952]
This commit is contained in:
Brezo Cordero
2021-12-08 16:34:27 -06:00
committed by Veljko V
parent b04cb41921
commit 06a4be4214
2 changed files with 211 additions and 41 deletions

View File

@@ -5,6 +5,7 @@ namespace MailPoet\Segments\DynamicSegments\Filters;
use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Util\DBCollationChecker;
use MailPoet\Util\Security;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
use MailPoetVendor\Doctrine\ORM\EntityManager;
@@ -15,10 +16,15 @@ class WooCommerceCountry implements Filter {
/** @var EntityManager */
private $entityManager;
/** @var DBCollationChecker */
private $collationChecker;
public function __construct(
EntityManager $entityManager
EntityManager $entityManager,
DBCollationChecker $collationChecker
) {
$this->entityManager = $entityManager;
$this->collationChecker = $collationChecker;
}
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
@@ -34,21 +40,24 @@ class WooCommerceCountry implements Filter {
}
$countryFilterParam = 'countryCode' . $filter->getId() ?? Security::generateRandomString();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$collation = $this->collationChecker->getCollateIfNeeded(
$subscribersTable,
'email',
$wpdb->prefix . 'wc_customer_lookup',
'email'
);
return $queryBuilder->innerJoin(
$subscribersTable,
$wpdb->postmeta,
'postmeta',
"postmeta.meta_key = '_customer_user' AND $subscribersTable.wp_user_id=postmeta.meta_value"
$wpdb->prefix . 'wc_customer_lookup',
'customer',
"$subscribersTable.email = customer.email $collation"
)->innerJoin(
'postmeta',
$wpdb->posts,
'posts',
"postmeta.post_id = posts.id AND posts.post_status NOT IN ('wc-cancelled', 'wc-failed')"
)->innerJoin(
'postmeta',
$wpdb->postmeta,
'postmetaCountry',
"postmeta.post_id = postmetaCountry.post_id AND postmetaCountry.meta_key = '_billing_country' AND postmetaCountry.meta_value = :$countryFilterParam"
)->setParameter($countryFilterParam, $countryCode[0]);
'customer',
$wpdb->prefix . 'wc_order_stats',
'orderStats',
'customer.customer_id = orderStats.customer_id'
)->where("customer.country = :$countryFilterParam")
->andWhere('orderStats.status NOT IN ("wc-cancelled", "wc-failed")')
->setParameter($countryFilterParam, $countryCode[0]);
}
}

View File

@@ -7,8 +7,7 @@ use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Doctrine\DBAL\Driver\Statement;
use MailPoetVendor\Doctrine\DBAL\ForwardCompatibility\DriverStatement;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
class WooCommerceCountryTest extends \MailPoetTest {
@@ -16,18 +15,12 @@ class WooCommerceCountryTest extends \MailPoetTest {
/** @var WooCommerceCountry */
private $wooCommerceCountry;
/** @var WPFunctions */
private $wp;
/** @var int[] */
private $orders;
/** @var SubscribersRepository */
private $subscribersRepository;
public function _before(): void {
self::createLookUpTables();
$this->wooCommerceCountry = $this->diContainer->get(WooCommerceCountry::class);
$this->wp = $this->diContainer->get(WPFunctions::class);
$this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$this->cleanup();
@@ -36,21 +29,22 @@ class WooCommerceCountryTest extends \MailPoetTest {
$userId2 = $this->tester->createWordPressUser('customer2@example.com', 'customer');
$userId3 = $this->tester->createWordPressUser('customer3@example.com', 'customer');
$this->orders[] = $this->createOrder(['user_id' => $userId1, 'billing_country' => 'US']);
$this->orders[] = $this->createOrder(['user_id' => $userId2, 'billing_country' => 'US']);
$this->orders[] = $this->createOrder(['user_id' => $userId3]);
$this->createCustomerLookupData(['user_id' => $userId1, 'email' => 'customer1@example.com', 'country' => 'CZ']);
$this->createCustomerLookupData(['user_id' => $userId2, 'email' => 'customer2@example.com', 'country' => 'US']);
$this->createCustomerLookupData(['user_id' => $userId3, 'email' => 'customer3@example.com', 'country' => 'US']);
}
public function testItAppliesFilter(): void {
$segmentFilter = $this->getSegmentFilter('CZ');
$queryBuilder = $this->wooCommerceCountry->apply($this->getQueryBuilder(), $segmentFilter);
$statement = $queryBuilder->execute();
$result = $statement instanceof Statement ? $statement->fetchAll() : [];
assert($statement instanceof DriverStatement);
$result = $statement->fetchAll();
expect(count($result))->equals(1);
$subscriber1 = $this->subscribersRepository->findOneById($result[0]['inner_subscriber_id']);
assert($subscriber1 instanceof SubscriberEntity);
expect($subscriber1)->isInstanceOf(SubscriberEntity::class);
expect($subscriber1->getEmail())->equals('customer3@example.com');
expect($subscriber1->getEmail())->equals('customer1@example.com');
}
private function getQueryBuilder(): QueryBuilder {
@@ -74,17 +68,40 @@ class WooCommerceCountryTest extends \MailPoetTest {
return $dynamicSegmentFilter;
}
private function createOrder($data): int {
return (int)$this->wp->wpInsertPost([
'post_type' => 'shop_order',
'post_status' => 'wc-completed',
'post_date' => $data['post_date'] ?? '',
'meta_input' => [
'_customer_user' => $data['user_id'] ?? '',
'_order_total' => $data['order_total'] ?? '1',
'_billing_country' => $data['billing_country'] ?? 'CZ',
],
]);
private function createCustomerLookupData(array $data) {
$connection = $this->entityManager->getConnection();
global $wpdb;
$customerLookupTable = $wpdb->prefix . 'wc_customer_lookup';
$connection->executeQuery("
INSERT INTO {$customerLookupTable} (user_id, first_name, last_name, email, country)
VALUES (
{$data['user_id']},
'',
'',
'{$data['email']}',
'{$data['country']}'
)
");
$id = $connection->lastInsertId();
$orderId = (int)$id + 1;
$orderLookupTable = $wpdb->prefix . 'wc_order_stats';
$connection->executeQuery("
INSERT INTO {$orderLookupTable} (order_id, status, customer_id)
VALUES (
{$orderId},
'wc-completed',
{$id}
)
");
}
private function cleanUpLookUpTables(): void {
$connection = $this->entityManager->getConnection();
global $wpdb;
$lookupTable = $wpdb->prefix . 'wc_customer_lookup';
$orderLookupTable = $wpdb->prefix . 'wc_order_stats';
$connection->executeStatement("TRUNCATE $lookupTable");
$connection->executeStatement("TRUNCATE $orderLookupTable");
}
public function _after(): void {
@@ -99,8 +116,152 @@ class WooCommerceCountryTest extends \MailPoetTest {
foreach ($emails as $email) {
$this->tester->deleteWordPressUser($email);
}
foreach ($this->orders ?? [] as $orderId) {
$this->wp->wpDeletePost($orderId);
$this->cleanUpLookUpTables();
}
/**
* Get WC Lookup Tables database schema.
* Copied from WC-Admin version 2.9.2-plugin
*
* @return string
*/
protected static function getSchema(): string {
global $wpdb;
$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
// Max DB index length. See wp_get_db_schema().
$maxIndexLength = 191;
return "
CREATE TABLE {$wpdb->prefix}wc_order_stats (
order_id bigint(20) unsigned NOT NULL,
parent_id bigint(20) unsigned DEFAULT 0 NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
num_items_sold int(11) DEFAULT 0 NOT NULL,
total_sales double DEFAULT 0 NOT NULL,
tax_total double DEFAULT 0 NOT NULL,
shipping_total double DEFAULT 0 NOT NULL,
net_total double DEFAULT 0 NOT NULL,
returning_customer boolean DEFAULT NULL,
status varchar(200) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (order_id),
KEY date_created (date_created),
KEY customer_id (customer_id),
KEY status (status({$maxIndexLength}))
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
order_item_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
variation_id BIGINT UNSIGNED NOT NULL,
customer_id BIGINT UNSIGNED NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT NOT NULL,
product_net_revenue double DEFAULT 0 NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
coupon_amount double DEFAULT 0 NOT NULL,
tax_amount double DEFAULT 0 NOT NULL,
shipping_amount double DEFAULT 0 NOT NULL,
shipping_tax_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_item_id),
KEY order_id (order_id),
KEY product_id (product_id),
KEY customer_id (customer_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
order_id BIGINT UNSIGNED NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL,
total_tax double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, tax_rate_id),
KEY tax_rate_id (tax_rate_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
order_id BIGINT UNSIGNED NOT NULL,
coupon_id BIGINT NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
discount_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, coupon_id),
KEY coupon_id (coupon_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
type varchar(20) NOT NULL,
locale varchar(20) NOT NULL,
title longtext NOT NULL,
content longtext NOT NULL,
content_data longtext NULL default null,
status varchar(200) NOT NULL,
source varchar(200) NOT NULL,
date_created datetime NOT NULL default '0000-00-00 00:00:00',
date_reminder datetime NULL default null,
is_snoozable boolean DEFAULT 0 NOT NULL,
layout varchar(20) DEFAULT '' NOT NULL,
image varchar(200) NULL DEFAULT NULL,
is_deleted boolean DEFAULT 0 NOT NULL,
is_read boolean DEFAULT 0 NOT NULL,
icon varchar(200) NOT NULL default 'info',
PRIMARY KEY (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
note_id BIGINT UNSIGNED NOT NULL,
name varchar(255) NOT NULL,
label varchar(255) NOT NULL,
query longtext NOT NULL,
status varchar(255) NOT NULL,
is_primary boolean DEFAULT 0 NOT NULL,
actioned_text varchar(255) NOT NULL,
nonce_action varchar(255) NULL DEFAULT NULL,
nonce_name varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (action_id),
KEY note_id (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED DEFAULT NULL,
username varchar(60) DEFAULT '' NOT NULL,
first_name varchar(255) NOT NULL,
last_name varchar(255) NOT NULL,
email varchar(100) NULL default NULL,
date_last_active timestamp NULL default null,
date_registered timestamp NULL default null,
country char(2) DEFAULT '' NOT NULL,
postcode varchar(20) DEFAULT '' NOT NULL,
city varchar(100) DEFAULT '' NOT NULL,
state varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (customer_id),
UNIQUE KEY user_id (user_id),
KEY email (email)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_category_lookup (
category_tree_id BIGINT UNSIGNED NOT NULL,
category_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (category_tree_id,category_id)
) $collate;
";
}
/**
* Create WC Lookup database tables.
*/
public static function createLookUpTables() {
if ((boolean)getenv('MULTISITE') === true) {
$wpUpgradePath = getenv('WP_ROOT_MULTISITE');
} else {
$wpUpgradePath = getenv('WP_ROOT');
}
require_once($wpUpgradePath . '/wp-admin/includes/upgrade.php');
dbDelta( self::getSchema() );
}
}