Use transient cache for count of subscribers in lists

[MAILPOET-3646]
This commit is contained in:
Jan Lysý
2021-07-01 09:39:25 +02:00
committed by Veljko V
parent 0a3971c045
commit 40a511c641
3 changed files with 194 additions and 95 deletions

View File

@ -1,7 +1,8 @@
<?php <?php declare(strict_types = 1);
namespace MailPoet\Segments; namespace MailPoet\Segments;
use MailPoet\Cache\TransientCache;
use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SegmentEntity;
@ -24,12 +25,17 @@ class SegmentSubscribersRepository {
/** @var FilterHandler */ /** @var FilterHandler */
private $filterHandler; private $filterHandler;
/** @var TransientCache */
private $transientCache;
public function __construct( public function __construct(
EntityManager $entityManager, EntityManager $entityManager,
FilterHandler $filterHandler FilterHandler $filterHandler,
TransientCache $transientCache
) { ) {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
$this->filterHandler = $filterHandler; $this->filterHandler = $filterHandler;
$this->transientCache = $transientCache;
} }
public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array { public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array {
@ -42,16 +48,8 @@ class SegmentSubscribersRepository {
public function getSubscribersCount(int $segmentId, string $status = null): int { public function getSubscribersCount(int $segmentId, string $status = null): int {
$segment = $this->getSegment($segmentId); $segment = $this->getSegment($segmentId);
$queryBuilder = $this->createCountQueryBuilder(); $result = $this->getSubscribersStatisticsCount($segment);
return (int)$result[$status ?: 'all'];
if ($segment->isStatic()) {
$queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, $status);
} else {
$queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, $status);
}
$statement = $this->executeQuery($queryBuilder);
$result = $statement->fetchColumn();
return (int)$result;
} }
public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null): int { public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null): int {
@ -101,11 +99,11 @@ class SegmentSubscribersRepository {
foreach ($filters as $filter) { foreach ($filters as $filter) {
$segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter)); $segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter));
} }
$queryBuilder = $this->createCountQueryBuilder(); $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
$queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null); $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null);
$statement = $this->executeQuery($queryBuilder); $statement = $this->executeQuery($queryBuilder);
$result = $statement->fetchColumn(); $result = $statement->fetch();
return (int)$result; return (int)$result['all'];
} }
private function createCountQueryBuilder(): QueryBuilder { private function createCountQueryBuilder(): QueryBuilder {
@ -117,18 +115,140 @@ class SegmentSubscribersRepository {
->from($subscribersTable); ->from($subscribersTable);
} }
public function getSubscribersWithoutSegmentCount(): int { private function createDynamicStatisticsQueryBuilder(): QueryBuilder {
$queryBuilder = $this->getSubscribersWithoutSegmentCountQuery(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
return (int)$queryBuilder->getQuery()->getSingleScalarResult(); return $this->entityManager
->getConnection()
->createQueryBuilder()
->from($subscribersTable)
->andWhere("$subscribersTable.deleted_at IS NULL")
->addSelect("COUNT(DISTINCT $subscribersTable.id) as `all`")
->addSelect("SUM(
CASE WHEN $subscribersTable.status = :status_subscribed
THEN 1 ELSE 0 END
) as :status_subscribed")
->addSelect("SUM(
CASE WHEN $subscribersTable.status = :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_unsubscribed")
->addSelect("SUM(
CASE WHEN $subscribersTable.status = :status_inactive
THEN 1 ELSE 0 END
) as :status_inactive")
->addSelect("SUM(
CASE WHEN $subscribersTable.status = :status_unconfirmed
THEN 1 ELSE 0 END
) as :status_unconfirmed")
->addSelect("SUM(
CASE WHEN $subscribersTable.status = :status_bounced
THEN 1 ELSE 0 END
) as :status_bounced")
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
} }
public function getSubscribersWithoutSegmentCountQuery(): ORMQueryBuilder { private function createStaticStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$queryBuilder = $this->entityManager
->getConnection()
->createQueryBuilder()
->from($subscriberSegmentTable, 'subscriber_segment')
->where('subscriber_segment.segment_id = :segment_id')
->setParameter('segment_id', $segment->getId())
->andWhere('subscribers.deleted_at is null')
->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
->addSelect("COUNT(DISTINCT subscribers.id) as `all`")
->addSelect('SUM(
CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed
THEN 1 ELSE 0 END
) as :status_subscribed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_unsubscribed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_inactive')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_unconfirmed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_bounced')
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
return $queryBuilder;
}
public function getSubscribersWithoutSegmentCount(): int {
$queryBuilder = $this->entityManager->createQueryBuilder(); $queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder $queryBuilder
->select('COUNT(DISTINCT s) AS subscribersCount') ->select('COUNT(DISTINCT s) AS subscribersCount')
->from(SubscriberEntity::class, 's'); ->from(SubscriberEntity::class, 's');
$this->addConstraintsForSubscribersWithoutSegment($queryBuilder); $this->addConstraintsForSubscribersWithoutSegment($queryBuilder);
return $queryBuilder; return (int)$queryBuilder->getQuery()->getSingleScalarResult();
}
public function getSubscribersWithoutSegmentStatisticsCount(): array {
$id = 0;
$result = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $id);
if (!$result) {
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$queryBuilder = $this->entityManager
->getConnection()
->createQueryBuilder();
$queryBuilder
->addSelect("SUM(
CASE WHEN s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as `all`")
->addSelect("SUM(
CASE WHEN s.deleted_at IS NOT NULL
THEN 1 ELSE 0 END
) as trash")
->addSelect("SUM(
CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as :status_subscribed")
->addSelect("SUM(
CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as :status_unsubscribed")
->addSelect("SUM(
CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as :status_inactive")
->addSelect("SUM(
CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as :status_unconfirmed")
->addSelect("SUM(
CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL
THEN 1 ELSE 0 END
) as :status_bounced")
->from($subscribersTable, 's')
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
$this->addConstraintsForSubscribersWithoutSegmentToDBAL($queryBuilder);
$statement = $this->executeQuery($queryBuilder);
$result = $statement->fetch();
$this->transientCache->setItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $result, $id);
}
return $result;
} }
public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void { public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void {
@ -149,6 +269,24 @@ class SegmentSubscribersRepository {
->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED); ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
} }
public function addConstraintsForSubscribersWithoutSegmentToDBAL(QueryBuilder $queryBuilder): void {
$deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
$subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
$deletedSegmentsQueryBuilder->select('sg.id')
->from(SegmentEntity::class, 'sg')
->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
$queryBuilder
->leftJoin('s', $subscribersSegmentTable, 'ssg',
(string)$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('ssg.subscriber_id', 's.id'),
$queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
$queryBuilder->expr()->notIn('ssg.segment_id', $deletedSegmentsQueryBuilder->getQuery()->getSQL())
))
->andWhere('ssg.id IS NULL')
->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
}
private function loadSubscriberIdsInSegment(int $segmentId, array $candidateIds = null): array { private function loadSubscriberIdsInSegment(int $segmentId, array $candidateIds = null): array {
$segment = $this->getSegment($segmentId); $segment = $this->getSegment($segmentId);
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
@ -242,45 +380,23 @@ class SegmentSubscribersRepository {
} }
public function getSubscribersStatisticsCount(SegmentEntity $segment) { public function getSubscribersStatisticsCount(SegmentEntity $segment) {
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); $result = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, (int)$segment->getId());
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); if (!$result) {
$queryBuilder = $this->entityManager if ($segment->isStatic()) {
->getConnection() $queryBuilder = $this->createStaticStatisticsQueryBuilder($segment);
->createQueryBuilder() } else {
->from($subscriberSegmentTable, 'subscriber_segment') $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
->where('subscriber_segment.segment_id = :segment_id') $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
->setParameter('segment_id', $segment->getId()) }
->andWhere('subscribers.deleted_at is null')
->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed
THEN 1 ELSE 0 END
) as :status_subscribed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_unsubscribed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_inactive')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_unconfirmed')
->addSelect('SUM(
CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed
THEN 1 ELSE 0 END
) as :status_bounced')
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED) $statement = $this->executeQuery($queryBuilder);
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED) $result = $statement->fetch();
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE) $this->transientCache->setItem(
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED) TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY,
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED); $result,
(int)$segment->getId()
$statement = $this->executeQuery($queryBuilder); );
$result = $statement->fetch(); }
return $result; return $result;
} }
} }

View File

@ -51,11 +51,12 @@ class SegmentsSimpleListRepository {
* @return array<array{id: string, name: string, type: string, subscribers: int}> * @return array<array{id: string, name: string, type: string, subscribers: int}>
*/ */
public function addVirtualSubscribersWithoutListSegment(array $segments): array { public function addVirtualSubscribersWithoutListSegment(array $segments): array {
$withoutSegmentStats = $this->segmentsSubscriberRepository->getSubscribersWithoutSegmentStatisticsCount();
$segments[] = [ $segments[] = [
'id' => '0', 'id' => '0',
'type' => SegmentEntity::TYPE_WITHOUT_LIST, 'type' => SegmentEntity::TYPE_WITHOUT_LIST,
'name' => __('Subscribers without a list', 'mailpoet'), 'name' => __('Subscribers without a list', 'mailpoet'),
'subscribers' => $this->segmentsSubscriberRepository->getSubscribersWithoutSegmentCount(), 'subscribers' => $withoutSegmentStats['all'],
]; ];
return $segments; return $segments;
} }

View File

@ -223,29 +223,24 @@ class SubscriberListingRepository extends ListingRepository {
public function getFilters(ListingDefinition $definition): array { public function getFilters(ListingDefinition $definition): array {
$group = $definition->getGroup(); $group = $definition->getGroup();
$queryBuilder = clone $this->queryBuilder; $subscribersWithoutSegmentStats = $this->segmentSubscribersRepository->getSubscribersWithoutSegmentStatisticsCount();
$this->applyFromClause($queryBuilder); $key = $group ?: 'all';
$subscribersWithoutSegmentQuery = $this->segmentSubscribersRepository->getSubscribersWithoutSegmentCountQuery(); $subscribersWithoutSegmentCount = $subscribersWithoutSegmentStats[$key];
if ($group) {
$this->applyGroup($queryBuilder, $group);
$this->applyGroup($subscribersWithoutSegmentQuery, $group);
}
$subscribersWithoutSegment = $subscribersWithoutSegmentQuery->getQuery()->getSingleScalarResult();
$subscribersWithoutSegmentLabel = sprintf( $subscribersWithoutSegmentLabel = sprintf(
WPFunctions::get()->__('Subscribers without a list (~%s)', 'mailpoet'), WPFunctions::get()->__('Subscribers without a list (~%s)', 'mailpoet'),
number_format((float)$subscribersWithoutSegment) number_format((float)$subscribersWithoutSegmentCount)
); );
$queryBuilder = clone $this->queryBuilder;
$queryBuilder $queryBuilder
->select('sg.id, sg.name, COUNT(s) AS subscribersCount') ->select('s')
->leftJoin('s.subscriberSegments', 'ssg') ->from(SegmentEntity::class, 's');
->join('ssg.segment', 'sg') if ($group === 'trash') {
->groupBy('sg.id') $queryBuilder->andWhere('s.deletedAt IS NOT NULL');
->andWhere('sg.deletedAt IS NULL') } else {
->andWhere('s.deletedAt IS NULL') $queryBuilder->andWhere('s.deletedAt IS NULL');
->having('subscribersCount > 0'); }
// format segment list // format segment list
$allSubscribersList = [ $allSubscribersList = [
@ -259,32 +254,19 @@ class SubscriberListingRepository extends ListingRepository {
]; ];
$segmentList = []; $segmentList = [];
foreach ($queryBuilder->getQuery()->getResult() as $item) {
$segmentList[] = [
'label' => sprintf('%s (~%s)', $item['name'], number_format((float)$item['subscribersCount'])),
'value' => $item['id'],
];
}
$queryBuilder = clone $this->queryBuilder;
// Load dynamic segments with some subscribers
$queryBuilder
->select('s')
->from(SegmentEntity::class, 's')
->andWhere('s.type = :dynamicType')
->andWhere('s.deletedAt IS NULL')
->setParameter('dynamicType', SegmentEntity::TYPE_DYNAMIC);
foreach ($queryBuilder->getQuery()->getResult() as $segment) { foreach ($queryBuilder->getQuery()->getResult() as $segment) {
$count = $this->segmentSubscribersRepository->getSubscribersCount($segment->getId()); $key = $group ?: 'all';
if (!$count) { $count = $this->segmentSubscribersRepository->getSubscribersStatisticsCount($segment);
if (!$count[$key]) {
continue; continue;
} }
$segmentList[] = [ $segmentList[] = [
'label' => sprintf('%s (~%s)', $segment->getName(), number_format((float)$count)), 'label' => sprintf('%s (~%s)', $segment->getName(), number_format((float)$count[$key])),
'value' => $segment->getId(), 'value' => $segment->getId(),
]; ];
} }
usort($segmentList, function($a, $b) { usort($segmentList, function($a, $b) {
return strcasecmp($a['label'], $b['label']); return strcasecmp($a['label'], $b['label']);
}); });