Skip to content

Commit a8fbca4

Browse files
committed
ManyToOne eager loading
1 parent 7f9e22f commit a8fbca4

16 files changed

+560
-32
lines changed

src/AbstractTDBMObject.php

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
*/
2323

2424
use JsonSerializable;
25+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery;
26+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
2527
use TheCodingMachine\TDBM\Schema\ForeignKeys;
2628
use TheCodingMachine\TDBM\Utils\ManyToManyRelationshipPathDescriptor;
2729

@@ -77,6 +79,13 @@ abstract class AbstractTDBMObject implements JsonSerializable
7779
*/
7880
private $manyToOneRelationships = [];
7981

82+
/**
83+
* If this bean originates from a ResultArray, this points back to the result array to build smart eager load queries.
84+
*
85+
* @var PartialQuery|null
86+
*/
87+
private $partialQuery;
88+
8089
/**
8190
* Used with $primaryKeys when we want to retrieve an existing object
8291
* and $primaryKeys=[] if we want a new object.
@@ -113,12 +122,13 @@ public function __construct(?string $tableName = null, array $primaryKeys = [],
113122
* @param array[] $beanData array<table, array<column, value>>
114123
* @param TDBMService $tdbmService
115124
*/
116-
public function _constructFromData(array $beanData, TDBMService $tdbmService): void
125+
public function _constructFromData(array $beanData, TDBMService $tdbmService, ?PartialQuery $partialQuery): void
117126
{
118127
$this->tdbmService = $tdbmService;
128+
$this->partialQuery = $partialQuery;
119129

120130
foreach ($beanData as $table => $columns) {
121-
$this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns);
131+
$this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns, $partialQuery);
122132
}
123133

124134
$this->status = TDBMObjectStateEnum::STATE_LOADED;
@@ -131,11 +141,12 @@ public function _constructFromData(array $beanData, TDBMService $tdbmService): v
131141
* @param mixed[] $primaryKeys
132142
* @param TDBMService $tdbmService
133143
*/
134-
public function _constructLazy(string $tableName, array $primaryKeys, TDBMService $tdbmService): void
144+
public function _constructLazy(string $tableName, array $primaryKeys, TDBMService $tdbmService, ?PartialQuery $partialQuery): void
135145
{
136146
$this->tdbmService = $tdbmService;
147+
$this->partialQuery = $partialQuery;
137148

138-
$this->dbRows[$tableName] = new DbRow($this, $tableName, static::getForeignKeys($tableName), $primaryKeys, $tdbmService);
149+
$this->dbRows[$tableName] = new DbRow($this, $tableName, static::getForeignKeys($tableName), $primaryKeys, $tdbmService, [], $partialQuery);
139150

140151
$this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
141152
}
@@ -179,7 +190,7 @@ public function _setStatus(string $state): void
179190
{
180191
$this->status = $state;
181192

182-
// The dirty state comes form the db_row itself so there is no need to set it from the called.
193+
// The dirty state comes from the db_row itself so there is no need to set it from the called.
183194
if ($state !== TDBMObjectStateEnum::STATE_DIRTY) {
184195
foreach ($this->dbRows as $dbRow) {
185196
$dbRow->_setStatus($state);
@@ -558,6 +569,20 @@ public function discardChanges(): void
558569
}
559570

560571
$this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
572+
foreach ($this->dbRows as $row) {
573+
$row->disableSmartEagerLoad();
574+
}
575+
$this->partialQuery = null;
576+
}
577+
578+
/**
579+
* Prevents smart eager loading of related entities.
580+
* If this bean was loaded through a result iterator, smart eager loading loads all entities of related beans at once.
581+
* You can disable it with this function.
582+
*/
583+
public function disableSmartEagerLoad(): void
584+
{
585+
$this->partialQuery = null;
561586
}
562587

563588
/**

src/DbRow.php

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
2222
*/
2323

24+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\ManyToOnePartialQuery;
25+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery;
26+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
2427
use TheCodingMachine\TDBM\Schema\ForeignKeys;
28+
use function array_pop;
29+
use function count;
2530

2631
/**
2732
* Instances of this class represent a row in a database.
@@ -77,7 +82,7 @@ class DbRow
7782

7883
/**
7984
* The values of the primary key.
80-
* This is set when the object is in "loaded" state.
85+
* This is set when the object is in "loaded" or "not loaded" state.
8186
*
8287
* @var array An array of column => value
8388
*/
@@ -100,6 +105,10 @@ class DbRow
100105
* @var ForeignKeys
101106
*/
102107
private $foreignKeys;
108+
/**
109+
* @var PartialQuery|null
110+
*/
111+
private $partialQuery;
103112

104113
/**
105114
* You should never call the constructor directly. Instead, you should use the
@@ -115,11 +124,12 @@ class DbRow
115124
* @param mixed[] $dbRow
116125
* @throws TDBMException
117126
*/
118-
public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = [])
127+
public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = [], ?PartialQuery $partialQuery = null)
119128
{
120129
$this->object = $object;
121130
$this->dbTableName = $tableName;
122131
$this->foreignKeys = $foreignKeys;
132+
$this->partialQuery = $partialQuery;
123133

124134
$this->status = TDBMObjectStateEnum::STATE_DETACHED;
125135

@@ -175,6 +185,15 @@ public function _setStatus(string $state) : void
175185
}
176186
}
177187

188+
/**
189+
* When discarding a bean, we expect to reload data from the DB, not the cache.
190+
* Hence, we must disable smart eager load.
191+
*/
192+
public function disableSmartEagerLoad(): void
193+
{
194+
$this->partialQuery = null;
195+
}
196+
178197
/**
179198
* This is an internal method. You should not call this method yourself. The TDBM library will do it for you.
180199
* If the object is in state 'not loaded', this method performs a query in database to load the object.
@@ -190,12 +209,30 @@ public function _dbLoadIfNotLoaded(): void
190209
}
191210
$connection = $this->tdbmService->getConnection();
192211

193-
list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform());
212+
if ($this->partialQuery !== null) {
213+
$this->partialQuery->registerDataLoader($connection);
194214

195-
$sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where;
196-
$result = $connection->executeQuery($sql, $parameters);
215+
// Let's get the data loader.
216+
$dataLoader = $this->partialQuery->getStorageNode()->getManyToOneDataLoader($this->partialQuery->getKey());
217+
218+
if (count($this->primaryKeys) !== 1) {
219+
throw new \RuntimeException('Dataloader patterns only supports primary keys on one columns. Table "'.$this->dbTableName.'" has a PK on '.count($this->primaryKeys). ' columns');
220+
}
221+
$pks = $this->primaryKeys;
222+
$pkId = array_pop($pks);
223+
224+
$row = $dataLoader->get((string) $pkId);
225+
} else {
226+
list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform());
227+
228+
$sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where;
229+
$result = $connection->executeQuery($sql, $parameters);
230+
231+
$row = $result->fetch(\PDO::FETCH_ASSOC);
232+
233+
$result->closeCursor();
234+
}
197235

198-
$row = $result->fetch(\PDO::FETCH_ASSOC);
199236

200237
if ($row === false) {
201238
throw new TDBMException("Could not retrieve object from table \"$this->dbTableName\" using filter \".$sql_where.\" with data \"".var_export($parameters, true)."\".");
@@ -208,8 +245,6 @@ public function _dbLoadIfNotLoaded(): void
208245
$this->dbRow[$key] = $types[$key]->convertToPHPValue($value, $connection->getDatabasePlatform());
209246
}
210247

211-
$result->closeCursor();
212-
213248
$this->status = TDBMObjectStateEnum::STATE_LOADED;
214249
}
215250
}
@@ -289,7 +324,8 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject
289324
$fk = $this->foreignKeys->getForeignKey($foreignKeyName);
290325

291326
$values = [];
292-
foreach ($fk->getUnquotedLocalColumns() as $column) {
327+
$localColumns = $fk->getUnquotedLocalColumns();
328+
foreach ($localColumns as $column) {
293329
if (!isset($this->dbRow[$column])) {
294330
return null;
295331
}
@@ -303,10 +339,18 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject
303339

304340
// If the foreign key points to the primary key, let's use findObjectByPk
305341
if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) {
306-
return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true);
342+
if ($this->partialQuery !== null && count($foreignColumns) === 1) {
343+
// Optimisation: let's build the smart eager load query we need to fetch more than one object at once.
344+
$newPartialQuery = new ManyToOnePartialQuery($this->partialQuery, $this->dbTableName, $fk->getForeignTableName(), $foreignColumns[0], $localColumns[0]);
345+
} else {
346+
$newPartialQuery = null;
347+
}
348+
$ref = $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, null, $newPartialQuery);
307349
} else {
308-
return $this->tdbmService->findObject($foreignTableName, $filter);
350+
$ref = $this->tdbmService->findObject($foreignTableName, $filter);
309351
}
352+
$this->references[$foreignKeyName] = $ref;
353+
return $ref;
310354
}
311355
}
312356

src/InnerResultArray.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
namespace TheCodingMachine\TDBM;
55

6-
use Doctrine\DBAL\Statement;
6+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\ManyToOneDataLoader;
7+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
8+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNodeTrait;
79

810
/*
911
Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
@@ -26,8 +28,10 @@
2628
/**
2729
* Iterator used to retrieve results. It behaves like an array.
2830
*/
29-
class InnerResultArray extends InnerResultIterator
31+
class InnerResultArray extends InnerResultIterator implements StorageNode
3032
{
33+
use StorageNodeTrait;
34+
3135
/**
3236
* The list of results already fetched.
3337
*

src/InnerResultIterator.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Mouf\Database\MagicQuery;
99
use Psr\Log\LoggerInterface;
1010
use Psr\Log\NullLogger;
11+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory;
12+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
1113
use TheCodingMachine\TDBM\Utils\DbalUtils;
1214

1315
/*
@@ -65,6 +67,10 @@ class InnerResultIterator implements \Iterator, \Countable, \ArrayAccess
6567
* @var LoggerInterface
6668
*/
6769
private $logger;
70+
/**
71+
* @var PartialQueryFactory|null
72+
*/
73+
private $partialQueryFactory;
6874

6975
protected $count = null;
7076

@@ -76,7 +82,7 @@ private function __construct()
7682
* @param mixed[] $parameters
7783
* @param array[] $columnDescriptors
7884
*/
79-
public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger): self
85+
public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger, ?PartialQueryFactory $partialQueryFactory): self
8086
{
8187
$iterator = new static();
8288
$iterator->magicSql = $magicSql;
@@ -90,6 +96,7 @@ public static function createInnerResultIterator(string $magicSql, array $parame
9096
$iterator->magicQuery = $magicQuery;
9197
$iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform();
9298
$iterator->logger = $logger;
99+
$iterator->partialQueryFactory = $partialQueryFactory;
93100
return $iterator;
94101
}
95102

@@ -201,6 +208,11 @@ public function next()
201208
$beansData[$columnDescriptor['tableGroup']][$columnDescriptor['table']][$columnDescriptor['column']] = $value;
202209
}
203210

211+
$partialQuery = null;
212+
if ($this instanceof StorageNode && $this->partialQueryFactory !== null) {
213+
$partialQuery = $this->partialQueryFactory->getPartialQuery($this, $this->magicQuery, $this->parameters);
214+
}
215+
204216
$reflectionClassCache = [];
205217
$firstBean = true;
206218
foreach ($beansData as $beanData) {
@@ -236,8 +248,9 @@ public function next()
236248
$reflectionClassCache[$actualClassName] = new \ReflectionClass($actualClassName);
237249
}
238250
// Let's bypass the constructor when creating the bean!
251+
/** @var AbstractTDBMObject $bean */
239252
$bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor();
240-
$bean->_constructFromData($beanData, $this->tdbmService);
253+
$bean->_constructFromData($beanData, $this->tdbmService, $partialQuery);
241254
}
242255

243256
// The first bean is the one containing the main table.

src/PageIterator.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Porpaginas\Page;
99
use Psr\Log\LoggerInterface;
1010
use Psr\Log\NullLogger;
11+
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory;
1112

1213
/*
1314
Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
@@ -49,6 +50,10 @@ class PageIterator implements Page, \ArrayAccess, \JsonSerializable
4950
private $offset;
5051
private $columnDescriptors;
5152
private $magicQuery;
53+
/**
54+
* @var PartialQueryFactory|null
55+
*/
56+
private $partialQueryFactory;
5257

5358
/**
5459
* The key of the current retrieved object.
@@ -76,7 +81,7 @@ private function __construct()
7681
* @param mixed[] $parameters
7782
* @param array[] $columnDescriptors
7883
*/
79-
public static function createResultIterator(ResultIterator $parentResult, string $magicSql, array $parameters, int $limit, int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, LoggerInterface $logger): self
84+
public static function createResultIterator(ResultIterator $parentResult, string $magicSql, array $parameters, int $limit, int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, LoggerInterface $logger, ?PartialQueryFactory $partialQueryFactory): self
8085
{
8186
$iterator = new self();
8287
$iterator->parentResult = $parentResult;
@@ -91,6 +96,7 @@ public static function createResultIterator(ResultIterator $parentResult, string
9196
$iterator->magicQuery = $magicQuery;
9297
$iterator->mode = $mode;
9398
$iterator->logger = $logger;
99+
$iterator->partialQueryFactory = $partialQueryFactory;
94100
return $iterator;
95101
}
96102

@@ -118,9 +124,9 @@ public function getIterator()
118124
if ($this->parentResult->count() === 0) {
119125
$this->innerResultIterator = InnerResultIterator::createEmpyIterator();
120126
} elseif ($this->mode === TDBMService::MODE_CURSOR) {
121-
$this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger);
127+
$this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->partialQueryFactory);
122128
} else {
123-
$this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger);
129+
$this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->partialQueryFactory);
124130
}
125131
}
126132

0 commit comments

Comments
 (0)