diff --git a/src/DataTables/Adapter/FetchJoinORMAdapter.php b/src/DataTables/Adapter/FetchJoinORMAdapter.php new file mode 100644 index 00000000..e3c04579 --- /dev/null +++ b/src/DataTables/Adapter/FetchJoinORMAdapter.php @@ -0,0 +1,145 @@ +use_simple_total = $options['simple_total_query']; + } + + protected function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + //Enforce object hydration mode (fetch join only works for objects) + $resolver->addAllowedValues('hydrate', Query::HYDRATE_OBJECT); + + /** + * Add the possibility to replace the query for total entity count through a very simple one, to improve performance. + * You can only use this option, if you did not apply any criteria to your total count. + */ + $resolver->setDefault('simple_total_query', false); + + return $resolver; + } + + protected function prepareQuery(AdapterQuery $query) + { + $state = $query->getState(); + $query->set('qb', $builder = $this->createQueryBuilder($state)); + $query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias()); + + // Provide default field mappings if needed + foreach ($state->getDataTable()->getColumns() as $column) { + if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) { + $column->setOption('field', "{$rootAlias}.{$name}"); + } + } + + /** @var Query\Expr\From $fromClause */ + $fromClause = $builder->getDQLPart('from')[0]; + $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}"; + + //Use simpler (faster) total count query if the user wanted so... + if ($this->use_simple_total) { + $query->setTotalRows($this->getSimpleTotalCount($builder)); + } else { + $query->setTotalRows($this->getCount($builder, $identifier)); + } + + // Get record count after filtering + $this->buildCriteria($builder, $state); + $query->setFilteredRows($this->getCount($builder, $identifier)); + + // Perform mapping of all referred fields and implied fields + $aliases = $this->getAliases($query); + $query->set('aliases', $aliases); + $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases)); + } + + public function getResults(AdapterQuery $query): \Traversable + { + $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 ($state->getLength() > 0) { + $builder + ->setFirstResult($state->getStart()) + ->setMaxResults($state->getLength()); + } + + $query = $builder->getQuery(); + $event = new ORMAdapterQueryEvent($query); + $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); + + //Use Doctrine paginator for result iteration + $paginator = new Paginator($query); + + foreach ($paginator->getIterator() as $result) { + yield $result; + $this->manager->detach($result); + } + } + + public function getCount(QueryBuilder $queryBuilder, $identifier) + { + $paginator = new Paginator($queryBuilder); + return $paginator->count(); + } + + protected function getSimpleTotalCount(QueryBuilder $queryBuilder) + { + /** The paginator count queries can be rather slow, so when query for total count (100ms or longer), + * just return the entity count. + */ + /** @var Query\Expr\From $from_expr */ + $from_expr = $queryBuilder->getDQLPart('from')[0]; + return $this->manager->getRepository($from_expr->getFrom())->count([]); + } +} \ No newline at end of file diff --git a/src/DataTables/Adapter/CustomORMAdapter.php b/src/DataTables/Adapter/ORMAdapter.php similarity index 79% rename from src/DataTables/Adapter/CustomORMAdapter.php rename to src/DataTables/Adapter/ORMAdapter.php index 5ce53663..a6fff5c0 100644 --- a/src/DataTables/Adapter/CustomORMAdapter.php +++ b/src/DataTables/Adapter/ORMAdapter.php @@ -1,64 +1,50 @@ + * @author Robbert Beesems */ -class CustomORMAdapter extends AbstractAdapter +class ORMAdapter extends AbstractAdapter { /** @var ManagerRegistry */ private $registry; /** @var EntityManager */ - private $manager; + protected $manager; /** @var \Doctrine\ORM\Mapping\ClassMetadata */ - private $metadata; + protected $metadata; /** @var int */ private $hydrationMode; @@ -69,10 +55,6 @@ class CustomORMAdapter extends AbstractAdapter /** @var QueryBuilderProcessorInterface[] */ protected $criteriaProcessors; - /** @var bool */ - protected $allow_fetch_join; - - /** * DoctrineAdapter constructor. */ @@ -108,7 +90,6 @@ class CustomORMAdapter extends AbstractAdapter $this->hydrationMode = $options['hydrate']; $this->queryBuilderProcessors = $options['query']; $this->criteriaProcessors = $options['criteria']; - $this->allow_fetch_join = $options['allow_fetch_join']; } /** @@ -135,13 +116,11 @@ class CustomORMAdapter extends AbstractAdapter /** @var Query\Expr\From $fromClause */ $fromClause = $builder->getDQLPart('from')[0]; $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}"; - - - $query->setTotalRows($this->getCount($builder, $identifier, true)); + $query->setTotalRows($this->getCount($builder, $identifier)); // Get record count after filtering $this->buildCriteria($builder, $state); - $query->setFilteredRows($this->getCount($builder, $identifier, false)); + $query->setFilteredRows($this->getCount($builder, $identifier)); // Perform mapping of all referred fields and implied fields $aliases = $this->getAliases($query); @@ -205,26 +184,18 @@ class CustomORMAdapter extends AbstractAdapter if ($state->getLength() > 0) { $builder ->setFirstResult($state->getStart()) - ->setMaxResults($state->getLength()); + ->setMaxResults($state->getLength()) + ; } $query = $builder->getQuery(); $event = new ORMAdapterQueryEvent($query); $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); - if ($this->allow_fetch_join && $this->hydrationMode === Query::HYDRATE_OBJECT) { - $paginator = new Paginator($query); - $iterator = $paginator->getIterator(); - } else { - $iterator = $query->iterate([], $this->hydrationMode); - } - - foreach ($iterator as $result) { + foreach ($query->iterate([], $this->hydrationMode) as $result) { + yield $entity = array_values($result)[0]; if (Query::HYDRATE_OBJECT === $this->hydrationMode) { - yield $entity = $result; $this->manager->detach($entity); - } else { - yield $entity = array_values($result)[0]; } } } @@ -253,22 +224,8 @@ class CustomORMAdapter extends AbstractAdapter * @param $identifier * @return int */ - protected function getCount(QueryBuilder $queryBuilder, $identifier, $total_count = false) + protected function getCount(QueryBuilder $queryBuilder, $identifier) { - if ($this->allow_fetch_join) { - /** The paginator count queries can be rather slow, so when query for total count (100ms or longer), - * just return the entity count. - */ - if ($total_count) { - /** @var Query\Expr\From $from_expr */ - $from_expr = $queryBuilder->getDQLPart('from')[0]; - return $this->manager->getRepository($from_expr->getFrom())->count([]); - } - - $paginator = new Paginator($queryBuilder); - return $paginator->count(); - } - $qb = clone $queryBuilder; $qb->resetDQLPart('orderBy'); @@ -305,7 +262,7 @@ class CustomORMAdapter extends AbstractAdapter * @param string $field * @return string */ - private function mapFieldToPropertyPath($field, array $aliases = []) + protected function mapFieldToPropertyPath($field, array $aliases = []) { $parts = explode('.', $field); if (count($parts) < 2) { @@ -338,7 +295,6 @@ class CustomORMAdapter extends AbstractAdapter $resolver ->setDefaults([ 'hydrate' => Query::HYDRATE_OBJECT, - 'allow_fetch_join' => false, 'query' => [], 'criteria' => function (Options $options) { return [new SearchCriteriaProvider()]; diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index d914f458..fe17453c 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace App\DataTables; use App\DataTables\Adapter\CustomORMAdapter; +use App\DataTables\Adapter\FetchJoinORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; @@ -215,8 +216,8 @@ final class PartsDataTable implements DataTableTypeInterface ]) ->addOrderBy('name') - ->createAdapter(CustomORMAdapter::class, [ - 'allow_fetch_join' => true, + ->createAdapter(FetchJoinORMAdapter::class, [ + 'simple_total_query' => true, 'query' => function (QueryBuilder $builder): void { $this->getQuery($builder); },