Skip to content

Commit ed00d89

Browse files
authored
Added an ORM adapter which allows fetch joins (#121)
1 parent 4cf2e8d commit ed00d89

File tree

3 files changed

+141
-3
lines changed

3 files changed

+141
-3
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
/*
3+
* Symfony DataTables Bundle
4+
* (c) Jan Böhmer 2020
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Omines\DataTablesBundle\Adapter\Doctrine;
13+
14+
use Doctrine\ORM\Query;
15+
use Doctrine\ORM\QueryBuilder;
16+
use Doctrine\ORM\Tools\Pagination\Paginator;
17+
use Omines\DataTablesBundle\Adapter\AdapterQuery;
18+
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
19+
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents;
20+
use Omines\DataTablesBundle\Column\AbstractColumn;
21+
use Symfony\Component\OptionsResolver\OptionsResolver;
22+
23+
/**
24+
* Similar to ORMAdapter this class allows to access objects from the doctrine ORM.
25+
* Unlike the default ORMAdapter supports Fetch Joins (additional entites are fetched from DB via joins) using
26+
* the Doctrine Paginator.
27+
* @author Jan Böhmer
28+
*/
29+
class FetchJoinORMAdapter extends ORMAdapter
30+
{
31+
protected $use_simple_total;
32+
33+
public function configure(array $options)
34+
{
35+
parent::configure($options);
36+
$this->use_simple_total = $options['simple_total_query'];
37+
}
38+
39+
protected function configureOptions(OptionsResolver $resolver)
40+
{
41+
parent::configureOptions($resolver);
42+
43+
//Enforce object hydration mode (fetch join only works for objects)
44+
$resolver->addAllowedValues('hydrate', Query::HYDRATE_OBJECT);
45+
46+
/**
47+
* Add the possibility to replace the query for total entity count through a very simple one, to improve performance.
48+
* You can only use this option, if you did not apply any criteria to your total count.
49+
*/
50+
$resolver->setDefault('simple_total_query', false);
51+
52+
return $resolver;
53+
}
54+
55+
protected function prepareQuery(AdapterQuery $query)
56+
{
57+
$state = $query->getState();
58+
$query->set('qb', $builder = $this->createQueryBuilder($state));
59+
$query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias());
60+
61+
// Provide default field mappings if needed
62+
foreach ($state->getDataTable()->getColumns() as $column) {
63+
if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) {
64+
$column->setOption('field', "{$rootAlias}.{$name}");
65+
}
66+
}
67+
68+
/** @var Query\Expr\From $fromClause */
69+
$fromClause = $builder->getDQLPart('from')[0];
70+
$identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
71+
72+
//Use simpler (faster) total count query if the user wanted so...
73+
if ($this->use_simple_total) {
74+
$query->setTotalRows($this->getSimpleTotalCount($builder));
75+
} else {
76+
$query->setTotalRows($this->getCount($builder, $identifier));
77+
}
78+
79+
// Get record count after filtering
80+
$this->buildCriteria($builder, $state);
81+
$query->setFilteredRows($this->getCount($builder, $identifier));
82+
83+
// Perform mapping of all referred fields and implied fields
84+
$aliases = $this->getAliases($query);
85+
$query->set('aliases', $aliases);
86+
$query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases));
87+
}
88+
89+
public function getResults(AdapterQuery $query): \Traversable
90+
{
91+
$builder = $query->get('qb');
92+
$state = $query->getState();
93+
94+
// Apply definitive view state for current 'page' of the table
95+
foreach ($state->getOrderBy() as list($column, $direction)) {
96+
/** @var AbstractColumn $column */
97+
if ($column->isOrderable()) {
98+
$builder->addOrderBy($column->getOrderField(), $direction);
99+
}
100+
}
101+
if ($state->getLength() > 0) {
102+
$builder
103+
->setFirstResult($state->getStart())
104+
->setMaxResults($state->getLength());
105+
}
106+
107+
$query = $builder->getQuery();
108+
$event = new ORMAdapterQueryEvent($query);
109+
$state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY);
110+
111+
//Use Doctrine paginator for result iteration
112+
$paginator = new Paginator($query);
113+
114+
foreach ($paginator->getIterator() as $result) {
115+
yield $result;
116+
$this->manager->detach($result);
117+
}
118+
}
119+
120+
public function getCount(QueryBuilder $queryBuilder, $identifier)
121+
{
122+
$paginator = new Paginator($queryBuilder);
123+
return $paginator->count();
124+
}
125+
126+
protected function getSimpleTotalCount(QueryBuilder $queryBuilder)
127+
{
128+
/** The paginator count queries can be rather slow, so when query for total count (100ms or longer),
129+
* just return the entity count.
130+
*/
131+
/** @var Query\Expr\From $from_expr */
132+
$from_expr = $queryBuilder->getDQLPart('from')[0];
133+
return $this->manager->getRepository($from_expr->getFrom())->count([]);
134+
}
135+
}

src/Adapter/Doctrine/ORMAdapter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ class ORMAdapter extends AbstractAdapter
4141
private $registry;
4242

4343
/** @var EntityManager */
44-
private $manager;
44+
protected $manager;
4545

4646
/** @var \Doctrine\ORM\Mapping\ClassMetadata */
47-
private $metadata;
47+
protected $metadata;
4848

4949
/** @var int */
5050
private $hydrationMode;
@@ -262,7 +262,7 @@ protected function hasGroupByPart($identifier, array $gbList)
262262
* @param string $field
263263
* @return string
264264
*/
265-
private function mapFieldToPropertyPath($field, array $aliases = [])
265+
protected function mapFieldToPropertyPath($field, array $aliases = [])
266266
{
267267
$parts = explode('.', $field);
268268
if (count($parts) < 2) {

src/Resources/config/services.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<service id="Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter">
1313
<argument type="service" id="doctrine" on-invalid="null" />
1414
</service>
15+
<service id="Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter">
16+
<argument type="service" id="doctrine" on-invalid="null" />
17+
</service>
1518

1619
<!-- Columns -->
1720
<service id="Omines\DataTablesBundle\Column\TwigColumn">

0 commit comments

Comments
 (0)