Include left join with domains when listing short URLs to avoid N+1 SELECT problem

This commit is contained in:
Alejandro Celaya 2024-10-27 11:20:44 +01:00
parent 3149adebdb
commit 84a187a26f
3 changed files with 26 additions and 13 deletions

View File

@ -264,7 +264,13 @@ class ShortUrl extends AbstractEntity
return true; return true;
} }
public function toArray(?VisitsSummary $precalculatedSummary = null): array /**
* @param null|(callable(): string|null) $getAuthority -
* This is a callback so that we trust its return value if provided, even if it is null.
* Providing the raw authority as `string|null` would result in a fallback to `$this->domain` when the authority
* was null.
*/
public function toArray(?VisitsSummary $precalculatedSummary = null, callable|null $getAuthority = null): array
{ {
return [ return [
'shortCode' => $this->shortCode, 'shortCode' => $this->shortCode,
@ -276,7 +282,7 @@ class ShortUrl extends AbstractEntity
'validUntil' => $this->validUntil?->toAtomString(), 'validUntil' => $this->validUntil?->toAtomString(),
'maxVisits' => $this->maxVisits, 'maxVisits' => $this->maxVisits,
], ],
'domain' => $this->domain, 'domain' => $getAuthority !== null ? $getAuthority() : $this->domain?->authority,
'title' => $this->title, 'title' => $this->title,
'crawlable' => $this->crawlable, 'crawlable' => $this->crawlable,
'forwardQuery' => $this->forwardQuery, 'forwardQuery' => $this->forwardQuery,

View File

@ -9,19 +9,26 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
final readonly class ShortUrlWithVisitsSummary final readonly class ShortUrlWithVisitsSummary
{ {
private function __construct(public ShortUrl $shortUrl, private ?VisitsSummary $visitsSummary = null) private function __construct(
{ public ShortUrl $shortUrl,
private VisitsSummary|null $visitsSummary = null,
private string|null $authority = null,
) {
} }
/** /**
* @param array{shortUrl: ShortUrl, visits: string|int, nonBotVisits: string|int} $data * @param array{shortUrl: ShortUrl, visits: string|int, nonBotVisits: string|int, authority: string|null} $data
*/ */
public static function fromArray(array $data): self public static function fromArray(array $data): self
{ {
return new self($data['shortUrl'], VisitsSummary::fromTotalAndNonBots( return new self(
(int) $data['visits'], shortUrl: $data['shortUrl'],
(int) $data['nonBotVisits'], visitsSummary: VisitsSummary::fromTotalAndNonBots(
)); total: (int) $data['visits'],
nonBots: (int) $data['nonBotVisits'],
),
authority: $data['authority'] ?? null,
);
} }
public static function fromShortUrl(ShortUrl $shortUrl): self public static function fromShortUrl(ShortUrl $shortUrl): self
@ -31,6 +38,6 @@ final readonly class ShortUrlWithVisitsSummary
public function toArray(): array public function toArray(): array
{ {
return $this->shortUrl->toArray($this->visitsSummary); return $this->shortUrl->toArray($this->visitsSummary, fn() => $this->authority);
} }
} }

View File

@ -43,7 +43,7 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
$qb = $this->createListQueryBuilder($filtering); $qb = $this->createListQueryBuilder($filtering);
$qb->select( $qb->select(
'DISTINCT s AS shortUrl', 'DISTINCT s AS shortUrl, d.authority',
'(' . $buildVisitsSubQuery('v', excludingBots: false) . ') AS ' . OrderableField::VISITS->value, '(' . $buildVisitsSubQuery('v', excludingBots: false) . ') AS ' . OrderableField::VISITS->value,
'(' . $buildVisitsSubQuery('v2', excludingBots: true) . ') AS ' . OrderableField::NON_BOT_VISITS->value, '(' . $buildVisitsSubQuery('v2', excludingBots: true) . ') AS ' . OrderableField::NON_BOT_VISITS->value,
// This is added only to have a consistent order by title between database engines // This is added only to have a consistent order by title between database engines
@ -89,6 +89,7 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(ShortUrl::class, 's') $qb->from(ShortUrl::class, 's')
->leftJoin('s.domain', 'd')
->where('1=1'); ->where('1=1');
$dateRange = $filtering->dateRange; $dateRange = $filtering->dateRange;
@ -129,8 +130,7 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
$conditions[] = $qb->expr()->like('t.name', ':searchPattern'); $conditions[] = $qb->expr()->like('t.name', ':searchPattern');
} }
$qb->leftJoin('s.domain', 'd') $qb->andWhere($qb->expr()->orX(...$conditions))
->andWhere($qb->expr()->orX(...$conditions))
->setParameter('searchPattern', '%' . $searchTerm . '%'); ->setParameter('searchPattern', '%' . $searchTerm . '%');
} }