From 83e6206eee6b7e9637e606049a93035fd5ce3710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 1 Dec 2024 01:02:40 +0100 Subject: [PATCH] Cache the results of the statistics to reduce database calls, especially on large databases --- .../Statistics/PaymentOrderStatistics.php | 205 +++++++++++------- templates/admin/dashboard.html.twig | 2 +- 2 files changed, 130 insertions(+), 77 deletions(-) diff --git a/src/Services/Statistics/PaymentOrderStatistics.php b/src/Services/Statistics/PaymentOrderStatistics.php index 2d67e6e..b57716c 100644 --- a/src/Services/Statistics/PaymentOrderStatistics.php +++ b/src/Services/Statistics/PaymentOrderStatistics.php @@ -8,11 +8,42 @@ use App\Entity\PaymentOrder; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +/** + * Helpers to retrieve various statistics about payment orders. + * The results get cached for 1 hour. + */ final readonly class PaymentOrderStatistics { - public function __construct(private EntityManagerInterface $entityManager) + //Cache time-to-live in seconds (1 hour) + private const CACHE_TTL = 3600; + + public function __construct(private EntityManagerInterface $entityManager, private CacheInterface $cache) + { + } + + /** + * Caches the result of a query for a given key (function name) and time range. + * @param string $key + * @param \DateTimeInterface|string|null $from + * @param \DateTimeInterface|string|null $to + * @param \Closure $callback + * @return mixed + */ + private function cached(string $key, null|\DateTimeInterface|string $from, null|\DateTimeInterface|string $to, \Closure $callback): mixed { + //Do not cache if datetime objects were passed directly (as we cannot really find them again in the cache) + if ($from instanceof \DateTimeInterface || $to instanceof \DateTimeInterface) { + return $callback($from, $to); + } + + $cacheKey = sprintf('po_statistics_%s_%s_%s', $key, $from ?? '0', $to ?? '0'); + return $this->cache->get($cacheKey, function (ItemInterface $item) use ($callback, $from, $to) { + $item->expiresAfter(3600); + return $callback($from, $to); + }); } private function addFromToRange(QueryBuilder $qb, null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null, string $field = "po.creation_date"): void @@ -40,13 +71,15 @@ private function addFromToRange(QueryBuilder $qb, null|\DateTimeInterface|string */ public function getSubmittedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('COUNT(po.id)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('submittedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('COUNT(po.id)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - return (int) $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); + }); } /** @@ -58,14 +91,16 @@ public function getSubmittedPaymentOrdersCount(null|\DateTimeInterface|string $f */ public function getBookedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('COUNT(po.id)') - ->from(PaymentOrder::class, 'po') - ->where('po.booking_date IS NOT NULL'); + return $this->cached('bookedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('COUNT(po.id)') + ->from(PaymentOrder::class, 'po') + ->where('po.booking_date IS NOT NULL'); - $this->addFromToRange($qb, $from, $to, "po.booking_date"); + $this->addFromToRange($qb, $from, $to, "po.booking_date"); - return (int) $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); + }); } /** @@ -77,14 +112,16 @@ public function getBookedPaymentOrdersCount(null|\DateTimeInterface|string $from */ public function getMathematicallyCheckedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('COUNT(po.id)') - ->from(PaymentOrder::class, 'po') - ->where('po.mathematically_correct.checked = true'); + return $this->cached('mathematicallyCheckedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('COUNT(po.id)') + ->from(PaymentOrder::class, 'po') + ->where('po.mathematically_correct.checked = true'); - $this->addFromToRange($qb, $from, $to, "po.mathematically_correct.timestamp"); + $this->addFromToRange($qb, $from, $to, "po.mathematically_correct.timestamp"); - return (int) $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); + }); } /** @@ -96,14 +133,16 @@ public function getMathematicallyCheckedPaymentOrdersCount(null|\DateTimeInterfa */ public function getFactuallyCheckedPaymentOrdersCount(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): int { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('COUNT(po.id)') - ->from(PaymentOrder::class, 'po') - ->where('po.factually_correct.checked = true'); + return $this->cached('factuallyCheckedCount', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('COUNT(po.id)') + ->from(PaymentOrder::class, 'po') + ->where('po.factually_correct.checked = true'); - $this->addFromToRange($qb, $from, $to, "po.factually_correct.timestamp"); + $this->addFromToRange($qb, $from, $to, "po.factually_correct.timestamp"); - return (int) $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); + }); } /** @@ -112,62 +151,72 @@ public function getFactuallyCheckedPaymentOrdersCount(null|\DateTimeInterface|st */ public function getPaymentOrdersTotalValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('SUM(po.amount)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('totalValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('SUM(po.amount)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - // We need to divide the result by 100 because the value is stored in cents. - return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + // We need to divide the result by 100 because the value is stored in cents. + return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + }); } public function getPaymentOrdersAverageValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('AVG(po.amount)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('averageValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('AVG(po.amount)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - // We need to divide the result by 100 because the value is stored in cents. - return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + // We need to divide the result by 100 because the value is stored in cents. + return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + }); } public function getPaymentOrdersStdDevValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('STDDEV(po.amount)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('stdDevValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('STDDEV(po.amount)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - // We need to divide the result by 100 because the value is stored in cents. - return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + // We need to divide the result by 100 because the value is stored in cents. + return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + }); } public function getPaymentOrdersMaxValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('MAX(po.amount)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('maxValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('MAX(po.amount)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - // We need to divide the result by 100 because the value is stored in cents. - return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + // We need to divide the result by 100 because the value is stored in cents. + return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + }); } public function getPaymentOrdersMinValue(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('MIN(po.amount)') - ->from(PaymentOrder::class, 'po'); + return $this->cached('minValue', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('MIN(po.amount)') + ->from(PaymentOrder::class, 'po'); - $this->addFromToRange($qb, $from, $to); + $this->addFromToRange($qb, $from, $to); - // We need to divide the result by 100 because the value is stored in cents. - return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + // We need to divide the result by 100 because the value is stored in cents. + return ((float) $qb->getQuery()->getSingleScalarResult()) / 100; + }); } /** @@ -178,20 +227,22 @@ public function getPaymentOrdersMinValue(null|\DateTimeInterface|string $from = */ public function getAverageProcessingTime(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): ?float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('AVG(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))') - ->from(PaymentOrder::class, 'po') - ->where('po.booking_date IS NOT NULL'); + return $this->cached('averageProcessingTime', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('AVG(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))') + ->from(PaymentOrder::class, 'po') + ->where('po.booking_date IS NOT NULL'); - $this->addFromToRange($qb, $from, $to, 'po.booking_date'); + $this->addFromToRange($qb, $from, $to, 'po.booking_date'); - $res = $qb->getQuery()->getSingleScalarResult(); - if ($res === null) { - return null; - } + $res = $qb->getQuery()->getSingleScalarResult(); + if ($res === null) { + return null; + } - //Res is in seconds, we need to convert it to days - return (float)$res / 86400; + //Res is in seconds, we need to convert it to days + return (float)$res / 86400; + }); } /** @@ -202,19 +253,21 @@ public function getAverageProcessingTime(null|\DateTimeInterface|string $from = */ public function getStddevProcessingTime(null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null): ?float { - $qb = $this->entityManager->createQueryBuilder(); - $qb->select('STDDEV(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))') - ->from(PaymentOrder::class, 'po') - ->where('po.booking_date IS NOT NULL'); + return $this->cached('stddevProcessingTime', $from, $to, function (null|\DateTimeInterface|string $from = null, null|\DateTimeInterface|string $to = null) { + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('STDDEV(UNIX_TIMESTAMP(po.booking_date) - UNIX_TIMESTAMP(po.creation_date))') + ->from(PaymentOrder::class, 'po') + ->where('po.booking_date IS NOT NULL'); - $this->addFromToRange($qb, $from, $to, 'po.booking_date'); + $this->addFromToRange($qb, $from, $to, 'po.booking_date'); - $res = $qb->getQuery()->getSingleScalarResult(); - if ($res === null) { - return null; - } + $res = $qb->getQuery()->getSingleScalarResult(); + if ($res === null) { + return null; + } - //Res is in seconds, we need to convert it to days - return (float)$res / 86400; + //Res is in seconds, we need to convert it to days + return (float)$res / 86400; + }); } } \ No newline at end of file diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig index 4ee561a..538eb09 100644 --- a/templates/admin/dashboard.html.twig +++ b/templates/admin/dashboard.html.twig @@ -88,7 +88,7 @@ -
Geldbeträge
+
Geldbeträge (eingereicht)