From d59b8817c3d319a7423f50cae130feddaca69f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Jul 2023 23:51:06 +0200 Subject: [PATCH] Do not use fetch join, as even with the N+1 problem the queries are faster than with the very complex and slow expressions needed for the fetch Join pagination --- .../Adapters/FetchResultsAtOnceORMAdapter.php | 69 +++++++++++++++++++ src/DataTables/PartsDataTable.php | 43 ++++++++---- src/Entity/Parts/PartLot.php | 3 +- 3 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php diff --git a/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php b/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php new file mode 100644 index 00000000..182dcbda --- /dev/null +++ b/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataTables\Adapters; + +use App\DataTables\Events\ORMPostQueryEvent; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\AdapterQuery; +use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents; +use Omines\DataTablesBundle\Column\AbstractColumn; + +/** + * This class is very similar to the original ORMAdapter, but instead of getting each line one by one, it gets all the results at once, + * which can save some time in combination with fetch hints. + */ +class FetchResultsAtOnceORMAdapter extends ORMAdapter +{ + protected function getResults(AdapterQuery $query): \Traversable + { + /** @var QueryBuilder $builder */ + $builder = $query->get('qb'); + $state = $query->getState(); + + // Apply definitive view state for current 'page' of the table + foreach ($state->getOrderBy() as list($column, $direction)) { + /** @var AbstractColumn $column */ + if ($column->isOrderable()) { + $builder->addOrderBy($column->getOrderField(), $direction); + } + } + if (null !== $state->getLength()) { + $builder + ->setFirstResult($state->getStart()) + ->setMaxResults($state->getLength()) + ; + } + + $q = $builder->getQuery(); + $event = new ORMAdapterQueryEvent($q); + $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); + + foreach ($q->getResult() as $item) { + yield $item; + } + } +} \ No newline at end of file diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 848b78a9..fcbe4982 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -22,11 +22,16 @@ declare(strict_types=1); namespace App\DataTables; +use App\DataTables\Adapters\FetchResultsAtOnceORMAdapter; use App\DataTables\Column\EnumColumn; use App\Entity\Parts\ManufacturingStatus; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Query; +use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Storelocation; -use App\DataTables\Adapters\CustomFetchJoinORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; @@ -43,12 +48,9 @@ use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Services\Formatters\AmountFormatter; -use App\Services\Attachments\AttachmentURLGenerator; use App\Services\EntityURLGenerator; -use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; -use Omines\DataTablesBundle\Column\MapColumn; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -267,12 +269,12 @@ final class PartsDataTable implements DataTableTypeInterface ]) ->addOrderBy('name') - ->createAdapter(CustomFetchJoinORMAdapter::class, [ - 'simple_total_query' => true, + ->createAdapter(FetchResultsAtOnceORMAdapter::class, [ 'query' => function (QueryBuilder $builder): void { $this->getQuery($builder); }, 'entity' => Part::class, + 'hydrate' => Query::HYDRATE_OBJECT, 'criteria' => [ function (QueryBuilder $builder) use ($options): void { $this->buildCriteria($builder, $options); @@ -280,13 +282,29 @@ final class PartsDataTable implements DataTableTypeInterface new SearchCriteriaProvider(), ], ]); + + $dataTable->addEventListener(ORMAdapterEvents::PRE_QUERY, $this->preQueryEventHandler(...)); + } + + private function preQueryEventHandler(ORMAdapterQueryEvent $event): void + { + $query = $event->getQuery(); + + //Eager fetch the associations of the part entity (we can only fetch toOne associations that way) + $query->setFetchMode(Part::class, 'category', ClassMetadataInfo::FETCH_EAGER); + $query->setFetchMode(Part::class, 'footprint', ClassMetadata::FETCH_EAGER); + $query->setFetchMode(Part::class, 'manufacturer', ClassMetadata::FETCH_EAGER); + $query->setFetchMode(Part::class, 'partUnit', ClassMetadata::FETCH_EAGER); + $query->setFetchMode(Part::class, 'master_picture_attachment', ClassMetadata::FETCH_EAGER); } private function getQuery(QueryBuilder $builder): void { //Distinct is very slow here, do not add this here (also I think this is not needed here, as the id column is always distinct) - $builder->select('part') - ->addSelect('category') + $builder + //->distinct() + ->select('part') + /*->addSelect('category') ->addSelect('footprint') ->addSelect('manufacturer') ->addSelect('partUnit') @@ -295,7 +313,7 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('partLots') ->addSelect('orderdetails') ->addSelect('attachments') - ->addSelect('storelocations') + ->addSelect('storelocations')*/ //Calculate amount sum using a subquery, so we can filter and sort by it ->addSelect( '( @@ -321,8 +339,9 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('part.parameters', 'parameters') //We have to group by all elements, or only the first sub elements of an association is fetched! (caused issue #190) - ->addGroupBy('part') - ->addGroupBy('partLots') + ->addGroupBy('part.id') + //->addGroupBy('part') + /*->addGroupBy('partLots') ->addGroupBy('category') ->addGroupBy('master_picture_attachment') ->addGroupBy('storelocations') @@ -333,7 +352,7 @@ final class PartsDataTable implements DataTableTypeInterface ->addGroupBy('suppliers') ->addGroupBy('attachments') ->addGroupBy('partUnit') - ->addGroupBy('parameters') + ->addGroupBy('parameters')*/ ; } diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index c54511f6..e552c06a 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Parts; +use App\Repository\PartLotRepository; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; @@ -79,7 +80,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named * @var Storelocation|null The storelocation of this lot */ #[Groups(['simple', 'extended', 'full', 'import'])] - #[ORM\ManyToOne(targetEntity: Storelocation::class)] + #[ORM\ManyToOne(targetEntity: Storelocation::class, fetch: 'EAGER')] #[ORM\JoinColumn(name: 'id_store_location')] #[Selectable()] protected ?Storelocation $storage_location = null;