Load visits and nonBotVisits via sub-queries in ShortUrlListRepository

This commit is contained in:
Alejandro Celaya 2024-03-25 19:22:13 +01:00
parent 7d415e40b2
commit 7afd3fd6a2
4 changed files with 53 additions and 39 deletions

View file

@ -38,4 +38,6 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE')
->build();
$builder->addUniqueConstraint(['short_url_id', 'potential_bot', 'slot_id'], 'UQ_slot_per_short_url');
};

View file

@ -30,30 +30,29 @@ final class Version20240318084804 extends AbstractMigration
$botsCount = $visitsQb->setParameter('potential_bot', '1')->executeQuery()->fetchOne();
$nonBotsCount = $visitsQb->setParameter('potential_bot', '0')->executeQuery()->fetchOne();
$this->connection->createQueryBuilder()
->insert('short_url_visits_counts')
->values([
'short_url_id' => ':short_url_id',
'count' => ':count',
'potential_bot' => '1',
])
->setParameters([
'short_url_id' => $shortUrlId,
'count' => $botsCount,
])
->executeStatement();
$this->connection->createQueryBuilder()
->insert('short_url_visits_counts')
->values([
'short_url_id' => ':short_url_id',
'count' => ':count',
'potential_bot' => '0',
])
->setParameters([
'short_url_id' => $shortUrlId,
'count' => $nonBotsCount,
])
->executeStatement();
if ($botsCount > 0) {
$this->insertCount($shortUrlId, $botsCount, potentialBot: true);
}
if ($nonBotsCount > 0) {
$this->insertCount($shortUrlId, $nonBotsCount, potentialBot: false);
}
}
}
private function insertCount(string $shortUrlId, int $count, bool $potentialBot): void
{
$this->connection->createQueryBuilder()
->insert('short_url_visits_counts')
->values([
'short_url_id' => ':short_url_id',
'count' => ':count',
'potential_bot' => ':potential_bot',
])
->setParameters([
'short_url_id' => $shortUrlId,
'count' => $count,
'potential_bot' => $potentialBot ? '1' : '0',
])
->executeStatement();
}
}

View file

@ -14,13 +14,13 @@ final readonly class ShortUrlWithVisitsSummary
}
/**
* @param array{shortUrl: ShortUrl, visitsCount: string|int, nonBotVisitsCount: string|int} $data
* @param array{shortUrl: ShortUrl, visits: string|int, nonBotVisits: string|int} $data
*/
public static function fromArray(array $data): self
{
return new self($data['shortUrl'], VisitsSummary::fromTotalAndNonBots(
(int) $data['visitsCount'],
(int) $data['nonBotVisitsCount'],
(int) $data['visits'],
(int) $data['nonBotVisits'],
));
}

View file

@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use function Shlinkio\Shlink\Core\ArrayUtils\map;
@ -27,21 +28,33 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
*/
public function findList(ShortUrlsListFiltering $filtering): array
{
$buildVisitsSubQuery = function (string $alias, bool $excludingBots): string {
$vqb = $this->getEntityManager()->createQueryBuilder();
$vqb->select('SUM(' . $alias . '.count)')
->from(ShortUrlVisitsCount::class, $alias)
->where($vqb->expr()->eq($alias . '.shortUrl', 's'));
if ($excludingBots) {
$vqb->andWhere($vqb->expr()->eq($alias . '.potentialBot', ':potentialBot'));
}
return $vqb->getDQL();
};
$qb = $this->createListQueryBuilder($filtering);
$qb->select('DISTINCT s AS shortUrl', 'SUM(v.count) AS visitsCount', 'SUM(v2.count) AS nonBotVisitsCount')
->addSelect('SUM(v.count)')
->leftJoin('s.visitsCounts', 'v')
->leftJoin('s.visitsCounts', 'v2', Join::WITH, $qb->expr()->andX(
$qb->expr()->eq('v.shortUrl', 's'),
$qb->expr()->eq('v.potentialBot', 'false'),
))
->groupBy('s')
$qb->select(
'DISTINCT s AS shortUrl',
'(' . $buildVisitsSubQuery('v', excludingBots: false) . ') AS ' . OrderableField::VISITS->value,
'(' . $buildVisitsSubQuery('v2', excludingBots: true) . ') AS ' . OrderableField::NON_BOT_VISITS->value,
)
->setMaxResults($filtering->limit)
->setFirstResult($filtering->offset);
->setFirstResult($filtering->offset)
// This param is used in one of the sub-queries, but needs to set in the parent query
->setParameter('potentialBot', 0);
$this->processOrderByForList($qb, $filtering);
/** @var array{shortUrl: ShortUrl, visitsCount: string, nonBotVisitsCount: string}[] $result */
/** @var array{shortUrl: ShortUrl, visits: string, nonBotVisits: string}[] $result */
$result = $qb->getQuery()->getResult();
return map($result, static fn (array $s) => ShortUrlWithVisitsSummary::fromArray($s));
}
@ -54,8 +67,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
match (true) {
// With no explicit order by, fallback to dateCreated-DESC
$fieldName === null => $qb->orderBy('s.dateCreated', 'DESC'),
$fieldName === OrderableField::VISITS->value => $qb->orderBy('SUM(v.count)', $order),
$fieldName === OrderableField::NON_BOT_VISITS->value => $qb->orderBy('SUM(v2.count)', $order),
$fieldName === OrderableField::VISITS->value,
$fieldName === OrderableField::NON_BOT_VISITS->value => $qb->orderBy($fieldName, $order),
default => $qb->orderBy('s.' . $fieldName, $order),
};
}