From d81f854ffb20c2405507fde5cb27fc9eaeed1e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 5 Sep 2019 13:54:52 +0200 Subject: [PATCH 1/9] ManyToOne eager loading --- src/AbstractTDBMObject.php | 35 +++++- src/DbRow.php | 66 +++++++++-- src/InnerResultArray.php | 8 +- src/InnerResultIterator.php | 17 ++- src/PageIterator.php | 12 +- src/QueryFactory/FindObjectsQueryFactory.php | 32 ++++- .../SmartEagerLoad/ManyToOneDataLoader.php | 66 +++++++++++ .../SmartEagerLoad/PartialQueryFactory.php | 17 +++ .../Query/ManyToOnePartialQuery.php | 94 +++++++++++++++ .../SmartEagerLoad/Query/PartialQuery.php | 34 ++++++ .../Query/StaticPartialQuery.php | 112 ++++++++++++++++++ .../SmartEagerLoad/StorageNode.php | 19 +++ .../SmartEagerLoad/StorageNodeTrait.php | 28 +++++ src/ResultIterator.php | 13 +- src/TDBMService.php | 15 ++- tests/TDBMDaoGeneratorTest.php | 24 ++++ 16 files changed, 560 insertions(+), 32 deletions(-) create mode 100644 src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php create mode 100644 src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php create mode 100644 src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php create mode 100644 src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php create mode 100644 src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php create mode 100644 src/QueryFactory/SmartEagerLoad/StorageNode.php create mode 100644 src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php diff --git a/src/AbstractTDBMObject.php b/src/AbstractTDBMObject.php index 10df8c56..36179c9a 100644 --- a/src/AbstractTDBMObject.php +++ b/src/AbstractTDBMObject.php @@ -22,6 +22,8 @@ */ use JsonSerializable; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Schema\ForeignKeys; use TheCodingMachine\TDBM\Utils\ManyToManyRelationshipPathDescriptor; @@ -77,6 +79,13 @@ abstract class AbstractTDBMObject implements JsonSerializable */ private $manyToOneRelationships = []; + /** + * If this bean originates from a ResultArray, this points back to the result array to build smart eager load queries. + * + * @var PartialQuery|null + */ + private $partialQuery; + /** * Used with $primaryKeys when we want to retrieve an existing object * and $primaryKeys=[] if we want a new object. @@ -113,12 +122,13 @@ public function __construct(?string $tableName = null, array $primaryKeys = [], * @param array[] $beanData array> * @param TDBMService $tdbmService */ - public function _constructFromData(array $beanData, TDBMService $tdbmService): void + public function _constructFromData(array $beanData, TDBMService $tdbmService, ?PartialQuery $partialQuery): void { $this->tdbmService = $tdbmService; + $this->partialQuery = $partialQuery; foreach ($beanData as $table => $columns) { - $this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns); + $this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns, $partialQuery); } $this->status = TDBMObjectStateEnum::STATE_LOADED; @@ -131,11 +141,12 @@ public function _constructFromData(array $beanData, TDBMService $tdbmService): v * @param mixed[] $primaryKeys * @param TDBMService $tdbmService */ - public function _constructLazy(string $tableName, array $primaryKeys, TDBMService $tdbmService): void + public function _constructLazy(string $tableName, array $primaryKeys, TDBMService $tdbmService, ?PartialQuery $partialQuery): void { $this->tdbmService = $tdbmService; + $this->partialQuery = $partialQuery; - $this->dbRows[$tableName] = new DbRow($this, $tableName, static::getForeignKeys($tableName), $primaryKeys, $tdbmService); + $this->dbRows[$tableName] = new DbRow($this, $tableName, static::getForeignKeys($tableName), $primaryKeys, $tdbmService, [], $partialQuery); $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED; } @@ -179,7 +190,7 @@ public function _setStatus(string $state): void { $this->status = $state; - // The dirty state comes form the db_row itself so there is no need to set it from the called. + // The dirty state comes from the db_row itself so there is no need to set it from the called. if ($state !== TDBMObjectStateEnum::STATE_DIRTY) { foreach ($this->dbRows as $dbRow) { $dbRow->_setStatus($state); @@ -558,6 +569,20 @@ public function discardChanges(): void } $this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED); + foreach ($this->dbRows as $row) { + $row->disableSmartEagerLoad(); + } + $this->partialQuery = null; + } + + /** + * Prevents smart eager loading of related entities. + * If this bean was loaded through a result iterator, smart eager loading loads all entities of related beans at once. + * You can disable it with this function. + */ + public function disableSmartEagerLoad(): void + { + $this->partialQuery = null; } /** diff --git a/src/DbRow.php b/src/DbRow.php index 84d2780e..3fab70d0 100644 --- a/src/DbRow.php +++ b/src/DbRow.php @@ -21,7 +21,12 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\ManyToOnePartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Schema\ForeignKeys; +use function array_pop; +use function count; /** * Instances of this class represent a row in a database. @@ -77,7 +82,7 @@ class DbRow /** * The values of the primary key. - * This is set when the object is in "loaded" state. + * This is set when the object is in "loaded" or "not loaded" state. * * @var array An array of column => value */ @@ -100,6 +105,10 @@ class DbRow * @var ForeignKeys */ private $foreignKeys; + /** + * @var PartialQuery|null + */ + private $partialQuery; /** * You should never call the constructor directly. Instead, you should use the @@ -115,11 +124,12 @@ class DbRow * @param mixed[] $dbRow * @throws TDBMException */ - public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = []) + public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = [], ?PartialQuery $partialQuery = null) { $this->object = $object; $this->dbTableName = $tableName; $this->foreignKeys = $foreignKeys; + $this->partialQuery = $partialQuery; $this->status = TDBMObjectStateEnum::STATE_DETACHED; @@ -175,6 +185,15 @@ public function _setStatus(string $state) : void } } + /** + * When discarding a bean, we expect to reload data from the DB, not the cache. + * Hence, we must disable smart eager load. + */ + public function disableSmartEagerLoad(): void + { + $this->partialQuery = null; + } + /** * This is an internal method. You should not call this method yourself. The TDBM library will do it for you. * 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 } $connection = $this->tdbmService->getConnection(); - list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform()); + if ($this->partialQuery !== null) { + $this->partialQuery->registerDataLoader($connection); - $sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where; - $result = $connection->executeQuery($sql, $parameters); + // Let's get the data loader. + $dataLoader = $this->partialQuery->getStorageNode()->getManyToOneDataLoader($this->partialQuery->getKey()); + + if (count($this->primaryKeys) !== 1) { + throw new \RuntimeException('Dataloader patterns only supports primary keys on one columns. Table "'.$this->dbTableName.'" has a PK on '.count($this->primaryKeys). ' columns'); + } + $pks = $this->primaryKeys; + $pkId = array_pop($pks); + + $row = $dataLoader->get((string) $pkId); + } else { + list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform()); + + $sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where; + $result = $connection->executeQuery($sql, $parameters); + + $row = $result->fetch(\PDO::FETCH_ASSOC); + + $result->closeCursor(); + } - $row = $result->fetch(\PDO::FETCH_ASSOC); if ($row === false) { 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 $this->dbRow[$key] = $types[$key]->convertToPHPValue($value, $connection->getDatabasePlatform()); } - $result->closeCursor(); - $this->status = TDBMObjectStateEnum::STATE_LOADED; } } @@ -289,7 +324,8 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject $fk = $this->foreignKeys->getForeignKey($foreignKeyName); $values = []; - foreach ($fk->getUnquotedLocalColumns() as $column) { + $localColumns = $fk->getUnquotedLocalColumns(); + foreach ($localColumns as $column) { if (!isset($this->dbRow[$column])) { return null; } @@ -303,10 +339,18 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject // If the foreign key points to the primary key, let's use findObjectByPk if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) { - return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true); + if ($this->partialQuery !== null && count($foreignColumns) === 1) { + // Optimisation: let's build the smart eager load query we need to fetch more than one object at once. + $newPartialQuery = new ManyToOnePartialQuery($this->partialQuery, $this->dbTableName, $fk->getForeignTableName(), $foreignColumns[0], $localColumns[0]); + } else { + $newPartialQuery = null; + } + $ref = $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, null, $newPartialQuery); } else { - return $this->tdbmService->findObject($foreignTableName, $filter); + $ref = $this->tdbmService->findObject($foreignTableName, $filter); } + $this->references[$foreignKeyName] = $ref; + return $ref; } } diff --git a/src/InnerResultArray.php b/src/InnerResultArray.php index 0e7a6ac3..845546df 100644 --- a/src/InnerResultArray.php +++ b/src/InnerResultArray.php @@ -3,7 +3,9 @@ namespace TheCodingMachine\TDBM; -use Doctrine\DBAL\Statement; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\ManyToOneDataLoader; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNodeTrait; /* Copyright (C) 2006-2017 David NĂ©grier - THE CODING MACHINE @@ -26,8 +28,10 @@ /** * Iterator used to retrieve results. It behaves like an array. */ -class InnerResultArray extends InnerResultIterator +class InnerResultArray extends InnerResultIterator implements StorageNode { + use StorageNodeTrait; + /** * The list of results already fetched. * diff --git a/src/InnerResultIterator.php b/src/InnerResultIterator.php index 0cb521a8..937e65e1 100644 --- a/src/InnerResultIterator.php +++ b/src/InnerResultIterator.php @@ -8,6 +8,8 @@ use Mouf\Database\MagicQuery; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Utils\DbalUtils; /* @@ -65,6 +67,10 @@ class InnerResultIterator implements \Iterator, \Countable, \ArrayAccess * @var LoggerInterface */ private $logger; + /** + * @var PartialQueryFactory|null + */ + private $partialQueryFactory; protected $count = null; @@ -76,7 +82,7 @@ private function __construct() * @param mixed[] $parameters * @param array[] $columnDescriptors */ - 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 + 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 { $iterator = new static(); $iterator->magicSql = $magicSql; @@ -90,6 +96,7 @@ public static function createInnerResultIterator(string $magicSql, array $parame $iterator->magicQuery = $magicQuery; $iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform(); $iterator->logger = $logger; + $iterator->partialQueryFactory = $partialQueryFactory; return $iterator; } @@ -201,6 +208,11 @@ public function next() $beansData[$columnDescriptor['tableGroup']][$columnDescriptor['table']][$columnDescriptor['column']] = $value; } + $partialQuery = null; + if ($this instanceof StorageNode && $this->partialQueryFactory !== null) { + $partialQuery = $this->partialQueryFactory->getPartialQuery($this, $this->magicQuery, $this->parameters); + } + $reflectionClassCache = []; $firstBean = true; foreach ($beansData as $beanData) { @@ -236,8 +248,9 @@ public function next() $reflectionClassCache[$actualClassName] = new \ReflectionClass($actualClassName); } // Let's bypass the constructor when creating the bean! + /** @var AbstractTDBMObject $bean */ $bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor(); - $bean->_constructFromData($beanData, $this->tdbmService); + $bean->_constructFromData($beanData, $this->tdbmService, $partialQuery); } // The first bean is the one containing the main table. diff --git a/src/PageIterator.php b/src/PageIterator.php index 61fb36ff..f6630539 100644 --- a/src/PageIterator.php +++ b/src/PageIterator.php @@ -8,6 +8,7 @@ use Porpaginas\Page; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory; /* Copyright (C) 2006-2017 David NĂ©grier - THE CODING MACHINE @@ -49,6 +50,10 @@ class PageIterator implements Page, \ArrayAccess, \JsonSerializable private $offset; private $columnDescriptors; private $magicQuery; + /** + * @var PartialQueryFactory|null + */ + private $partialQueryFactory; /** * The key of the current retrieved object. @@ -76,7 +81,7 @@ private function __construct() * @param mixed[] $parameters * @param array[] $columnDescriptors */ - 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 + 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 { $iterator = new self(); $iterator->parentResult = $parentResult; @@ -91,6 +96,7 @@ public static function createResultIterator(ResultIterator $parentResult, string $iterator->magicQuery = $magicQuery; $iterator->mode = $mode; $iterator->logger = $logger; + $iterator->partialQueryFactory = $partialQueryFactory; return $iterator; } @@ -118,9 +124,9 @@ public function getIterator() if ($this->parentResult->count() === 0) { $this->innerResultIterator = InnerResultIterator::createEmpyIterator(); } elseif ($this->mode === TDBMService::MODE_CURSOR) { - $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->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); } else { - $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->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); } } diff --git a/src/QueryFactory/FindObjectsQueryFactory.php b/src/QueryFactory/FindObjectsQueryFactory.php index fbaf4c44..39d7305a 100644 --- a/src/QueryFactory/FindObjectsQueryFactory.php +++ b/src/QueryFactory/FindObjectsQueryFactory.php @@ -6,14 +6,20 @@ use Doctrine\Common\Cache\Cache; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\Schema; +use Mouf\Database\MagicQuery; +use TheCodingMachine\TDBM\InnerResultArray; use TheCodingMachine\TDBM\OrderByAnalyzer; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\StaticPartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\TDBMService; use function implode; /** * This class is in charge of creating the MagicQuery SQL based on parameters passed to findObjects method. */ -class FindObjectsQueryFactory extends AbstractQueryFactory +class FindObjectsQueryFactory extends AbstractQueryFactory implements PartialQueryFactory { private $additionalTablesFetch; private $filterString; @@ -81,4 +87,28 @@ protected function compute(): void $this->columnDescList, ]); } + + /** + * Generates a SQL query to be used as a sub-query. + * @param array $parameters + */ + public function getPartialQuery(StorageNode $storageNode, MagicQuery $magicQuery, array $parameters): PartialQuery + { + $mysqlPlatform = new MySqlPlatform(); + + // Special case: if the main table is part of an inheritance relationship, we need to get all related tables + $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($this->mainTable); + if (count($relatedTables) === 1) { + $sql = 'FROM '.$mysqlPlatform->quoteIdentifier($this->mainTable); + } else { + // Let's use MagicQuery to build the query + $sql = 'FROM MAGICJOIN('.$this->mainTable.')'; + } + + if (!empty($this->filterString)) { + $sql .= ' WHERE '.$this->filterString; + } + + return new StaticPartialQuery($sql, $parameters, $relatedTables, $storageNode, $magicQuery); + } } diff --git a/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php b/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php new file mode 100644 index 00000000..3f8eeb39 --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php @@ -0,0 +1,66 @@ +> Rows, indexed by ID. + */ + private $data; + + public function __construct(Connection $connection, string $sql, string $idColumn) + { + $this->connection = $connection; + $this->sql = $sql; + $this->idColumn = $idColumn; + } + + /** + * @return array> Rows, indexed by ID. + */ + private function load(): array + { + $results = $this->connection->fetchAll($this->sql); + $results = array_column($results, null, $this->idColumn); + + return $results; + } + + /** + * Returns the DB row with the given ID. + * Loads all rows if necessary. + * Throws an exception if nothing found. + * + * @param string $id + * @return array + */ + public function get(string $id): array + { + if ($this->data === null) { + $this->data = $this->load(); + } + + if (!isset($this->data[$id])) { + throw new TDBMException('The loaded dataset does not contain row with ID "'.$id.'"'); + } + return $this->data[$id]; + } +} diff --git a/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php b/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php new file mode 100644 index 00000000..bb9035d1 --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php @@ -0,0 +1,17 @@ + $parameters + */ + public function getPartialQuery(StorageNode $storageNode, MagicQuery $magicQuery, array $parameters) : PartialQuery; +} diff --git a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php new file mode 100644 index 00000000..ea2f1276 --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php @@ -0,0 +1,94 @@ +queryFrom = 'FROM ' .$mysqlPlatform->quoteIdentifier($tableName). + ' WHERE ' .$mysqlPlatform->quoteIdentifier($tableName).'.'.$mysqlPlatform->quoteIdentifier($pk).' IN '. + '(SELECT '.$mysqlPlatform->quoteIdentifier($originTableName).'.'.$mysqlPlatform->quoteIdentifier($columnName).' '.$partialQuery->getQueryFrom().')'; + $this->mainTable = $tableName; + $this->storageNode = $partialQuery->getStorageNode(); + $this->key = $partialQuery->getKey().'__'.$columnName; + $this->pk = $pk; + } + + /** + * Returns the SQL of the query, starting at the FROM keyword. + */ + public function getQueryFrom(): string + { + return $this->queryFrom; + } + + /** + * Returns the name of the main table (main objects returned by this query) + */ + public function getMainTable(): string + { + return $this->mainTable; + } + + /** + * Returns the object in charge of storing the dataloader associated to this query. + */ + public function getStorageNode(): StorageNode + { + return $this->storageNode; + } + + /** + * Returns a key representing the "path" to this query. This is meant to be used as a cache key. + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Registers a dataloader for this query, if needed. + */ + public function registerDataLoader(Connection $connection): void + { + if ($this->storageNode->hasManyToOneDataLoader($this->key)) { + return; + } + + $mysqlPlatform = new MySqlPlatform(); + $sql = 'SELECT DISTINCT ' .$mysqlPlatform->quoteIdentifier($this->mainTable).'.* '.$this->queryFrom; + + $this->storageNode->setManyToOneDataLoader($this->key, new ManyToOneDataLoader($connection, $sql, $this->pk)); + } +} diff --git a/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php new file mode 100644 index 00000000..de866dbe --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php @@ -0,0 +1,34 @@ + + */ + private $parameters; + /** + * @var MagicQuery + */ + private $magicQuery; + /** + * @var string + */ + private $magicFrom; + + /** + * @param array $parameters + * @param string[] $mainTables + */ + public function __construct(string $queryFrom, array $parameters, array $mainTables, StorageNode $storageNode, MagicQuery $magicQuery) + { + $this->queryFrom = $queryFrom; + $this->mainTables = $mainTables; + $this->storageNode = $storageNode; + $this->parameters = $parameters; + $this->magicQuery = $magicQuery; + } + + /** + * Returns the SQL of the query, starting at the FROM keyword. + */ + public function getQueryFrom(): string + { + if ($this->magicFrom === null) { + // FIXME: we need to use buildPreparedStatement for better performances here. + $sql = 'SELECT '; + $mysqlPlatform = new MySqlPlatform(); + $tables = []; + foreach ($this->mainTables as $table) { + $tables[] = $mysqlPlatform->quoteIdentifier($table).'.*'; + } + $sql .= implode(', ', $tables); + $sql .= ' '.$this->queryFrom; + + $sql = $this->magicQuery->build($sql, $this->parameters); + $fromIndex = strpos($sql, 'FROM'); + if ($fromIndex === false) { + throw new TDBMException('Expected smart eager loader query to contain a "FROM"'); + } + $this->magicFrom = substr($sql, $fromIndex); + } + return $this->magicFrom; + } + + /** + * Returns a key representing the "path" to this query. This is meant to be used as a cache key. + */ + public function getKey(): string + { + return ''; + } + + /** + * Registers a dataloader for this query, if needed. + */ + public function registerDataLoader(Connection $connection): void + { + throw new TDBMException('Cannot register a dataloader for root query'); + } + + /** + * Returns the object in charge of storing the dataloader associated to this query. + */ + public function getStorageNode(): StorageNode + { + return $this->storageNode; + } + + /** + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/src/QueryFactory/SmartEagerLoad/StorageNode.php b/src/QueryFactory/SmartEagerLoad/StorageNode.php new file mode 100644 index 00000000..6ebd5abf --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/StorageNode.php @@ -0,0 +1,19 @@ + + */ + private $manyToOneDataLoaders = []; + + public function getManyToOneDataLoader(string $key): ManyToOneDataLoader + { + return $this->manyToOneDataLoaders[$key]; + } + + public function hasManyToOneDataLoader(string $key): bool + { + return isset($this->manyToOneDataLoaders[$key]); + } + + public function setManyToOneDataLoader(string $key, ManyToOneDataLoader $manyToOneDataLoader): void + { + $this->manyToOneDataLoaders[$key] = $manyToOneDataLoader; + } +} diff --git a/src/ResultIterator.php b/src/ResultIterator.php index de0eec47..cae40979 100644 --- a/src/ResultIterator.php +++ b/src/ResultIterator.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Platforms\MySqlPlatform; use Psr\Log\NullLogger; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory; use function array_map; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Statement; @@ -172,10 +173,13 @@ public function getIterator() if ($this->innerResultIterator === null) { if ($this->totalCount === 0) { $this->innerResultIterator = InnerResultArray::createEmpyIterator(); - } elseif ($this->mode === TDBMService::MODE_CURSOR) { - $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); } else { - $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); + $partialQueryFactory = $this->queryFactory instanceof PartialQueryFactory ? $this->queryFactory : null; + if ($this->mode === TDBMService::MODE_CURSOR) { + $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $partialQueryFactory); + } else { + $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $partialQueryFactory); + } } } @@ -193,7 +197,8 @@ public function take($offset, $limit) if ($this->totalCount === 0) { return PageIterator::createEmpyIterator($this); } - return PageIterator::createResultIterator($this, $this->queryFactory->getMagicSql(), $this->parameters, $limit, $offset, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->mode, $this->logger); + $partialQueryFactory = $this->queryFactory instanceof PartialQueryFactory ? $this->queryFactory : null; + return PageIterator::createResultIterator($this, $this->queryFactory->getMagicSql(), $this->parameters, $limit, $offset, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->mode, $this->logger, $partialQueryFactory); } /** diff --git a/src/TDBMService.php b/src/TDBMService.php index ad33b10c..87503390 100644 --- a/src/TDBMService.php +++ b/src/TDBMService.php @@ -39,6 +39,8 @@ use TheCodingMachine\TDBM\QueryFactory\FindObjectsFromSqlQueryFactory; use TheCodingMachine\TDBM\QueryFactory\FindObjectsQueryFactory; use TheCodingMachine\TDBM\QueryFactory\FindObjectsFromRawSqlQueryFactory; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Utils\ManyToManyRelationshipPathDescriptor; use TheCodingMachine\TDBM\Utils\NamingStrategyInterface; use TheCodingMachine\TDBM\Utils\TDBMDaoGenerator; @@ -1188,7 +1190,7 @@ public function findObjectsFromSql(string $mainTable, string $from, $filter = nu * * @throws TDBMException */ - public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch = array(), bool $lazy = false, string $className = null): AbstractTDBMObject + public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch = array(), bool $lazy = false, string $className = null, ?PartialQuery $partialQuery = null): AbstractTDBMObject { $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys); $hash = $this->getObjectHash($primaryKeys); @@ -1222,9 +1224,9 @@ public function findObjectByPk(string $table, array $primaryKeys, array $additio $this->reflectionClassCache[$className] = new \ReflectionClass($className); } // Let's bypass the constructor when creating the bean! - /** @var AbstractTDBMObject */ + /** @var AbstractTDBMObject $bean */ $bean = $this->reflectionClassCache[$className]->newInstanceWithoutConstructor(); - $bean->_constructLazy($table, $primaryKeys, $this); + $bean->_constructLazy($table, $primaryKeys, $this, $partialQuery); return $bean; } @@ -1257,7 +1259,12 @@ public function findObjectByPk(string $table, array $primaryKeys, array $additio public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null) : ?AbstractTDBMObject { $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className); - return $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters); + $object = $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters); + if ($object !== null) { + // Smart eager loading on a result set of at most one result is useless. + $object->disableSmartEagerLoad(); + } + return $object; } /** diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index e1e4d4e8..a5c09b6c 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -2205,6 +2205,9 @@ public function testSubQueryWithFind(): void $this->assertSame('Foo', $results[0]->getContent()); } + /** + * @depends testDaoGeneration + */ public function testSubQueryExceptionOnPrimaryKeysWithMultipleColumns(): void { $stateDao = new StateDao($this->tdbmService); @@ -2213,4 +2216,25 @@ public function testSubQueryExceptionOnPrimaryKeysWithMultipleColumns(): void $this->expectExceptionMessage('You cannot use in a sub-query a table that has a primary key on more that 1 column.'); $states->_getSubQuery(); } + + public function testManyToOneEagerLoading(): void + { + $userDao = new UserDao($this->tdbmService); + $users = $userDao->findAll(); + $countryIds = []; + foreach ($users as $user) { + $countryIds[] = $user->getCountry()->getId(); + } + + $this->assertFalse($users->getIterator()->hasManyToOneDataLoader('__country_id')); + $this->assertSame([2, 1, 3, 2, 2, 4], $countryIds); + + $countryNames = []; + foreach ($users as $user) { + $countryNames[] = $user->getCountry()->getLabel(); + } + + $this->assertTrue($users->getIterator()->hasManyToOneDataLoader('__country_id')); + $this->assertSame(['UK', 'France', 'Jamaica', 'UK', 'UK', 'Mexico'], $countryNames); + } } From aeda807de6264f72d4f4fb56d6de86a21280054e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 5 Sep 2019 14:40:59 +0200 Subject: [PATCH 2/9] Fixing PostgreSQL dialect in MagicQuery --- src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php index ce3394b8..60563e31 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySqlPlatform; use Mouf\Database\MagicQuery; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; @@ -68,7 +69,9 @@ public function getQueryFrom(): string $sql .= implode(', ', $tables); $sql .= ' '.$this->queryFrom; + $this->magicQuery->setOutputDialect($mysqlPlatform); $sql = $this->magicQuery->build($sql, $this->parameters); + $this->magicQuery->setOutputDialect(null); $fromIndex = strpos($sql, 'FROM'); if ($fromIndex === false) { throw new TDBMException('Expected smart eager loader query to contain a "FROM"'); From 9baee5a3b89c73e1fbb74ffb0878593a86ea4eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 5 Sep 2019 14:42:46 +0200 Subject: [PATCH 3/9] Coding Style fix --- src/DbRow.php | 8 +++++--- src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php | 1 - src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php | 1 - .../SmartEagerLoad/Query/ManyToOnePartialQuery.php | 1 - .../SmartEagerLoad/Query/StaticPartialQuery.php | 1 - src/QueryFactory/SmartEagerLoad/StorageNode.php | 2 +- src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php | 1 - 7 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/DbRow.php b/src/DbRow.php index 3fab70d0..dadf9586 100644 --- a/src/DbRow.php +++ b/src/DbRow.php @@ -27,6 +27,7 @@ use TheCodingMachine\TDBM\Schema\ForeignKeys; use function array_pop; use function count; +use function var_export; /** * Instances of this class represent a row in a database. @@ -231,12 +232,13 @@ public function _dbLoadIfNotLoaded(): void $row = $result->fetch(\PDO::FETCH_ASSOC); $result->closeCursor(); + + if ($row === false) { + throw new TDBMException("Could not retrieve object from table \"$this->dbTableName\" using filter \".$sql_where.\" with data \"".var_export($parameters, true)."\"."); + } } - if ($row === false) { - throw new TDBMException("Could not retrieve object from table \"$this->dbTableName\" using filter \".$sql_where.\" with data \"".var_export($parameters, true)."\"."); - } $this->dbRow = []; $types = $this->tdbmService->_getColumnTypesForTable($this->dbTableName); diff --git a/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php b/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php index 3f8eeb39..b730b66e 100644 --- a/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php +++ b/src/QueryFactory/SmartEagerLoad/ManyToOneDataLoader.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad; - use Doctrine\DBAL\Connection; use TheCodingMachine\TDBM\TDBMException; diff --git a/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php b/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php index bb9035d1..b77944c4 100644 --- a/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php +++ b/src/QueryFactory/SmartEagerLoad/PartialQueryFactory.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad; - use Mouf\Database\MagicQuery; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; diff --git a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php index ea2f1276..dcc29c0b 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query; - use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\MySqlPlatform; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\ManyToOneDataLoader; diff --git a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php index 60563e31..156ad38c 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query; - use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySqlPlatform; diff --git a/src/QueryFactory/SmartEagerLoad/StorageNode.php b/src/QueryFactory/SmartEagerLoad/StorageNode.php index 6ebd5abf..a67fb42f 100644 --- a/src/QueryFactory/SmartEagerLoad/StorageNode.php +++ b/src/QueryFactory/SmartEagerLoad/StorageNode.php @@ -16,4 +16,4 @@ public function setManyToOneDataLoader(string $key, ManyToOneDataLoader $manyToO // TODO: getManyToOneDataLoader($key) / hasManyToOneDataLoader($key) / setManyToOneDataLoader($key) ... // ... getOneToManyDataLoader ... -} \ No newline at end of file +} diff --git a/src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php b/src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php index 7f6f3c33..44396da8 100644 --- a/src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php +++ b/src/QueryFactory/SmartEagerLoad/StorageNodeTrait.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad; - trait StorageNodeTrait { /** From d8b675d472986bf3e2db2d0fa0f5f7b667b50405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 6 Sep 2019 11:28:21 +0200 Subject: [PATCH 4/9] Fixing output dialect --- .../Query/ManyToOnePartialQuery.php | 57 +++++++++++-------- .../SmartEagerLoad/Query/PartialQuery.php | 3 + .../Query/StaticPartialQuery.php | 5 ++ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php index dcc29c0b..8291a595 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\MySqlPlatform; +use Mouf\Database\MagicQuery; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\ManyToOneDataLoader; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; @@ -13,35 +14,37 @@ class ManyToOnePartialQuery implements PartialQuery /** * @var string */ - private $queryFrom; + private $mainTable; /** * @var string */ - private $mainTable; + private $key; /** - * @var StorageNode + * @var string */ - private $storageNode; + private $pk; + /** + * @var PartialQuery + */ + private $partialQuery; /** * @var string */ - private $key; + private $originTableName; /** * @var string */ - private $pk; + private $columnName; public function __construct(PartialQuery $partialQuery, string $originTableName, string $tableName, string $pk, string $columnName) { // TODO: move this in a separate function. The constructor is called for every bean. - $mysqlPlatform = new MySqlPlatform(); - $this->queryFrom = 'FROM ' .$mysqlPlatform->quoteIdentifier($tableName). - ' WHERE ' .$mysqlPlatform->quoteIdentifier($tableName).'.'.$mysqlPlatform->quoteIdentifier($pk).' IN '. - '(SELECT '.$mysqlPlatform->quoteIdentifier($originTableName).'.'.$mysqlPlatform->quoteIdentifier($columnName).' '.$partialQuery->getQueryFrom().')'; + $this->partialQuery = $partialQuery; $this->mainTable = $tableName; - $this->storageNode = $partialQuery->getStorageNode(); $this->key = $partialQuery->getKey().'__'.$columnName; $this->pk = $pk; + $this->originTableName = $originTableName; + $this->columnName = $columnName; } /** @@ -49,15 +52,10 @@ public function __construct(PartialQuery $partialQuery, string $originTableName, */ public function getQueryFrom(): string { - return $this->queryFrom; - } - - /** - * Returns the name of the main table (main objects returned by this query) - */ - public function getMainTable(): string - { - return $this->mainTable; + $mysqlPlatform = new MySqlPlatform(); + return 'FROM ' .$mysqlPlatform->quoteIdentifier($this->mainTable). + ' WHERE ' .$mysqlPlatform->quoteIdentifier($this->mainTable).'.'.$mysqlPlatform->quoteIdentifier($this->pk).' IN '. + '(SELECT '.$mysqlPlatform->quoteIdentifier($this->originTableName).'.'.$mysqlPlatform->quoteIdentifier($this->columnName).' '.$this->partialQuery->getQueryFrom().')'; } /** @@ -65,7 +63,7 @@ public function getMainTable(): string */ public function getStorageNode(): StorageNode { - return $this->storageNode; + return $this->partialQuery->getStorageNode(); } /** @@ -81,13 +79,24 @@ public function getKey(): string */ public function registerDataLoader(Connection $connection): void { - if ($this->storageNode->hasManyToOneDataLoader($this->key)) { + $storageNode = $this->getStorageNode(); + if ($storageNode->hasManyToOneDataLoader($this->key)) { return; } $mysqlPlatform = new MySqlPlatform(); - $sql = 'SELECT DISTINCT ' .$mysqlPlatform->quoteIdentifier($this->mainTable).'.* '.$this->queryFrom; + $sql = 'SELECT DISTINCT ' .$mysqlPlatform->quoteIdentifier($this->mainTable).'.* '.$this->getQueryFrom(); - $this->storageNode->setManyToOneDataLoader($this->key, new ManyToOneDataLoader($connection, $sql, $this->pk)); + if (!$connection->getDatabasePlatform() instanceof MySqlPlatform) { + // We need to convert the query from MySQL dialect to something else + $sql = $this->getMagicQuery()->buildPreparedStatement($sql); + } + + $storageNode->setManyToOneDataLoader($this->key, new ManyToOneDataLoader($connection, $sql, $this->pk)); + } + + public function getMagicQuery(): MagicQuery + { + return $this->partialQuery->getMagicQuery(); } } diff --git a/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php index de866dbe..d2533b5c 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/PartialQuery.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query; use Doctrine\DBAL\Connection; +use Mouf\Database\MagicQuery; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; /** @@ -31,4 +32,6 @@ public function registerDataLoader(Connection $connection): void; * Returns the object in charge of storing the dataloader associated to this query. */ public function getStorageNode(): StorageNode; + + public function getMagicQuery(): MagicQuery; } diff --git a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php index 156ad38c..aa731775 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php @@ -111,4 +111,9 @@ public function getParameters(): array { return $this->parameters; } + + public function getMagicQuery(): MagicQuery + { + return $this->magicQuery; + } } From e34752bfd73a3b5211afb11ff4f3d84a871750f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 6 Sep 2019 11:40:25 +0200 Subject: [PATCH 5/9] Fixing PgSQL related test (order issue) --- tests/TDBMDaoGeneratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index a5c09b6c..84c5236f 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -2220,7 +2220,7 @@ public function testSubQueryExceptionOnPrimaryKeysWithMultipleColumns(): void public function testManyToOneEagerLoading(): void { $userDao = new UserDao($this->tdbmService); - $users = $userDao->findAll(); + $users = $userDao->findAll()->withOrder('id asc'); $countryIds = []; foreach ($users as $user) { $countryIds[] = $user->getCountry()->getId(); From 558dcab939a399c9c4bd9a4fdbba16ff7ed9c08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 6 Sep 2019 16:50:12 +0200 Subject: [PATCH 6/9] Improving code coverage and exception throwing for lazy beans --- src/DbRow.php | 4 +-- src/QueryFactory/FindObjectsQueryFactory.php | 4 ++- .../Query/StaticPartialQuery.php | 2 +- .../ManyToOneDataLoaderTest.php | 21 ++++++++++++ .../Query/StaticPartialQueryTest.php | 34 +++++++++++++++++++ tests/TDBMAbstractServiceTest.php | 8 ++--- tests/TDBMDaoGeneratorTest.php | 18 ++++++++++ 7 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php create mode 100644 tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php diff --git a/src/DbRow.php b/src/DbRow.php index dadf9586..d04d52e4 100644 --- a/src/DbRow.php +++ b/src/DbRow.php @@ -217,7 +217,7 @@ public function _dbLoadIfNotLoaded(): void $dataLoader = $this->partialQuery->getStorageNode()->getManyToOneDataLoader($this->partialQuery->getKey()); if (count($this->primaryKeys) !== 1) { - throw new \RuntimeException('Dataloader patterns only supports primary keys on one columns. Table "'.$this->dbTableName.'" has a PK on '.count($this->primaryKeys). ' columns'); + throw new \RuntimeException('Data-loader patterns only supports primary keys on one column. Table "'.$this->dbTableName.'" has a PK on '.count($this->primaryKeys). ' columns'); // @codeCoverageIgnore } $pks = $this->primaryKeys; $pkId = array_pop($pks); @@ -234,7 +234,7 @@ public function _dbLoadIfNotLoaded(): void $result->closeCursor(); if ($row === false) { - throw new TDBMException("Could not retrieve object from table \"$this->dbTableName\" using filter \".$sql_where.\" with data \"".var_export($parameters, true)."\"."); + throw new NoBeanFoundException("Could not retrieve object from table \"$this->dbTableName\" using filter \"$sql_where\" with data \"".var_export($parameters, true). '".'); } } diff --git a/src/QueryFactory/FindObjectsQueryFactory.php b/src/QueryFactory/FindObjectsQueryFactory.php index 39d7305a..4121bc42 100644 --- a/src/QueryFactory/FindObjectsQueryFactory.php +++ b/src/QueryFactory/FindObjectsQueryFactory.php @@ -43,7 +43,8 @@ protected function compute(): void [ $this->magicSql, $this->magicSqlCount, - $this->columnDescList + $this->columnDescList, + $this->magicSqlSubQuery ] = $this->cache->fetch($key); return; } @@ -85,6 +86,7 @@ protected function compute(): void $this->magicSql, $this->magicSqlCount, $this->columnDescList, + $this->magicSqlSubQuery, ]); } diff --git a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php index aa731775..c8e56b2a 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/StaticPartialQuery.php @@ -73,7 +73,7 @@ public function getQueryFrom(): string $this->magicQuery->setOutputDialect(null); $fromIndex = strpos($sql, 'FROM'); if ($fromIndex === false) { - throw new TDBMException('Expected smart eager loader query to contain a "FROM"'); + throw new TDBMException('Expected smart eager loader query to contain a "FROM"'); // @codeCoverageIgnore } $this->magicFrom = substr($sql, $fromIndex); } diff --git a/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php b/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php new file mode 100644 index 00000000..9fd80a55 --- /dev/null +++ b/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php @@ -0,0 +1,21 @@ +createMock(Connection::class); + $connection->method('fetchAll')->willReturn([]); + $dataLoader = new ManyToOneDataLoader($connection, 'SELECT * FROM users', 'id'); + + $this->expectException(TDBMException::class); + $dataLoader->get('42'); + } +} diff --git a/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php b/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php new file mode 100644 index 00000000..4e7ee9e8 --- /dev/null +++ b/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php @@ -0,0 +1,34 @@ +42], ['users'], $this->createMock(StorageNode::class), new MagicQuery()); + $this->expectException(TDBMException::class); + $query->registerDataLoader($this->createMock(Connection::class)); + } + + public function testGetMagicQuery() + { + $magicQuery = new MagicQuery(); + $query = new StaticPartialQuery('FROM users', ['foo'=>42], ['users'], $this->createMock(StorageNode::class), $magicQuery); + $this->assertSame($magicQuery, $query->getMagicQuery()); + } + + public function testGetParameters() + { + $magicQuery = new MagicQuery(); + $query = new StaticPartialQuery('FROM users', ['foo'=>42], ['users'], $this->createMock(StorageNode::class), $magicQuery); + $this->assertSame(['foo'=>42], $query->getParameters()); + } +} diff --git a/tests/TDBMAbstractServiceTest.php b/tests/TDBMAbstractServiceTest.php index 79a2c202..24b9ff04 100644 --- a/tests/TDBMAbstractServiceTest.php +++ b/tests/TDBMAbstractServiceTest.php @@ -69,7 +69,7 @@ abstract class TDBMAbstractServiceTest extends TestCase /** * @var ArrayCache */ - private $cache; + private static $cache; public static function setUpBeforeClass(): void { @@ -133,10 +133,10 @@ protected function getDummyGeneratorListener() : DummyGeneratorListener protected function getCache(): ArrayCache { - if ($this->cache === null) { - $this->cache = new ArrayCache(); + if (self::$cache === null) { + self::$cache = new ArrayCache(); } - return $this->cache; + return self::$cache; } protected function getConfiguration() : ConfigurationInterface diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index 84c5236f..95acd185 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -2217,6 +2217,9 @@ public function testSubQueryExceptionOnPrimaryKeysWithMultipleColumns(): void $states->_getSubQuery(); } + /** + * @depends testDaoGeneration + */ public function testManyToOneEagerLoading(): void { $userDao = new UserDao($this->tdbmService); @@ -2237,4 +2240,19 @@ public function testManyToOneEagerLoading(): void $this->assertTrue($users->getIterator()->hasManyToOneDataLoader('__country_id')); $this->assertSame(['UK', 'France', 'Jamaica', 'UK', 'UK', 'Mexico'], $countryNames); } + + /** + * @depends testDaoGeneration + */ + public function testLazyLoadBadIdException(): void + { + $countryDao = new CountryDao($this->tdbmService); + $lazyBean = $countryDao->getById(-1, true); + + $this->expectException(NoBeanFoundException::class); + $this->expectExceptionMessage("Could not retrieve object from table \"country\" using filter \"(`id` = :tdbmparam1)\" with data \"array ( + 'tdbmparam1' => -1, +)\"."); + $lazyBean->getLabel(); + } } From f51198bf1e127d5e4fb119ad39df5544b8a7535c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 6 Sep 2019 17:14:55 +0200 Subject: [PATCH 7/9] test fix for PGSQL + CS --- tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php | 1 - .../SmartEagerLoad/Query/StaticPartialQueryTest.php | 1 - tests/TDBMDaoGeneratorTest.php | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php b/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php index 9fd80a55..60cdc447 100644 --- a/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php +++ b/tests/QueryFactory/SmartEagerLoad/ManyToOneDataLoaderTest.php @@ -8,7 +8,6 @@ class ManyToOneDataLoaderTest extends TestCase { - public function testGet() { $connection = $this->createMock(Connection::class); diff --git a/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php b/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php index 4e7ee9e8..cbdbaeb6 100644 --- a/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php +++ b/tests/QueryFactory/SmartEagerLoad/Query/StaticPartialQueryTest.php @@ -10,7 +10,6 @@ class StaticPartialQueryTest extends TestCase { - public function testRegisterDataLoader() { $query = new StaticPartialQuery('FROM users', ['foo'=>42], ['users'], $this->createMock(StorageNode::class), new MagicQuery()); diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index 95acd185..6bae87b5 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -2250,9 +2250,7 @@ public function testLazyLoadBadIdException(): void $lazyBean = $countryDao->getById(-1, true); $this->expectException(NoBeanFoundException::class); - $this->expectExceptionMessage("Could not retrieve object from table \"country\" using filter \"(`id` = :tdbmparam1)\" with data \"array ( - 'tdbmparam1' => -1, -)\"."); + $this->expectExceptionMessage("Could not retrieve object from table"); $lazyBean->getLabel(); } } From a25a8f6bcf4bc4c6a1ce619bf03e0b4d43b6fae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 9 Sep 2019 16:01:06 +0200 Subject: [PATCH 8/9] Generating partial query only once --- src/InnerResultIterator.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/InnerResultIterator.php b/src/InnerResultIterator.php index 937e65e1..cb3d44ba 100644 --- a/src/InnerResultIterator.php +++ b/src/InnerResultIterator.php @@ -9,6 +9,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\PartialQueryFactory; +use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery; use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Utils\DbalUtils; @@ -68,11 +69,13 @@ class InnerResultIterator implements \Iterator, \Countable, \ArrayAccess */ private $logger; /** - * @var PartialQueryFactory|null + * @var PartialQuery|null */ - private $partialQueryFactory; - - protected $count = null; + private $partialQuery; + /** + * @var int|null + */ + protected $count; private function __construct() { @@ -96,7 +99,11 @@ public static function createInnerResultIterator(string $magicSql, array $parame $iterator->magicQuery = $magicQuery; $iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform(); $iterator->logger = $logger; - $iterator->partialQueryFactory = $partialQueryFactory; + $partialQuery = null; + if ($iterator instanceof StorageNode && $partialQueryFactory !== null) { + $iterator->partialQuery = $partialQueryFactory->getPartialQuery($iterator, $magicQuery, $parameters); + } + return $iterator; } @@ -208,11 +215,6 @@ public function next() $beansData[$columnDescriptor['tableGroup']][$columnDescriptor['table']][$columnDescriptor['column']] = $value; } - $partialQuery = null; - if ($this instanceof StorageNode && $this->partialQueryFactory !== null) { - $partialQuery = $this->partialQueryFactory->getPartialQuery($this, $this->magicQuery, $this->parameters); - } - $reflectionClassCache = []; $firstBean = true; foreach ($beansData as $beanData) { @@ -250,7 +252,7 @@ public function next() // Let's bypass the constructor when creating the bean! /** @var AbstractTDBMObject $bean */ $bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor(); - $bean->_constructFromData($beanData, $this->tdbmService, $partialQuery); + $bean->_constructFromData($beanData, $this->tdbmService, $this->partialQuery); } // The first bean is the one containing the main table. From a816e5b9944ad0ad4bda4a77985f3262894770e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 11 Sep 2019 17:36:40 +0200 Subject: [PATCH 9/9] Refactoring retrieveManyToOneRelationshipsStorage signature This will allow us in the future to build a oneToMany handler for smart eager loading. --- src/AbstractTDBMObject.php | 17 ++++- .../SmartEagerLoad/OneToManyDataLoader.php | 69 +++++++++++++++++++ .../Query/ManyToOnePartialQuery.php | 3 +- src/Utils/BeanDescriptor.php | 22 +----- .../DirectForeignKeyMethodDescriptor.php | 30 +++----- src/Utils/Psr2Utils.php | 52 ++++++++++++++ tests/TDBMDaoGeneratorTest.php | 22 +++++- 7 files changed, 166 insertions(+), 49 deletions(-) create mode 100644 src/QueryFactory/SmartEagerLoad/OneToManyDataLoader.php create mode 100644 src/Utils/Psr2Utils.php diff --git a/src/AbstractTDBMObject.php b/src/AbstractTDBMObject.php index 36179c9a..8518338e 100644 --- a/src/AbstractTDBMObject.php +++ b/src/AbstractTDBMObject.php @@ -26,6 +26,7 @@ use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode; use TheCodingMachine\TDBM\Schema\ForeignKeys; use TheCodingMachine\TDBM\Utils\ManyToManyRelationshipPathDescriptor; +use function array_combine; /** * Instances of this class represent a "bean". Usually, a bean is mapped to a row of one table. @@ -531,12 +532,15 @@ private function removeManyToOneRelationship(string $tableName, string $foreignK * * @param string $tableName * @param string $foreignKeyName - * @param mixed[] $searchFilter - * @param string $orderString The ORDER BY part of the query. All columns must be prefixed by the table name (in the form: table.column). WARNING : This parameter is not kept when there is an additionnal or removal object ! + * @param array $localColumns + * @param array $foreignColumns + * @param string $foreignTableName + * @param string $orderString The ORDER BY part of the query. All columns must be prefixed by the table name (in the form: table.column). WARNING : This parameter is not kept when there is an additional or removal object ! * * @return AlterableResultIterator + * @throws TDBMException */ - protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $searchFilter, string $orderString = null) : AlterableResultIterator + protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $localColumns, array $foreignColumns, string $foreignTableName, string $orderString = null) : AlterableResultIterator { $key = $tableName.'___'.$foreignKeyName; $alterableResultIterator = $this->getManyToOneAlterableResultIterator($tableName, $foreignKeyName); @@ -544,6 +548,13 @@ protected function retrieveManyToOneRelationshipsStorage(string $tableName, stri return $alterableResultIterator; } + $ids = []; + foreach ($foreignColumns as $foreignColumn) { + $ids[] = $this->get($foreignColumn, $foreignTableName); + } + + $searchFilter = array_combine($localColumns, $ids); + $unalteredResultIterator = $this->tdbmService->findObjects($tableName, $searchFilter, [], $orderString); $alterableResultIterator->setResultIterator($unalteredResultIterator->getIterator()); diff --git a/src/QueryFactory/SmartEagerLoad/OneToManyDataLoader.php b/src/QueryFactory/SmartEagerLoad/OneToManyDataLoader.php new file mode 100644 index 00000000..c4d0bfa6 --- /dev/null +++ b/src/QueryFactory/SmartEagerLoad/OneToManyDataLoader.php @@ -0,0 +1,69 @@ +>> Array of rows, indexed by foreign key. + */ + private $data; + + public function __construct(Connection $connection, string $sql, string $fkColumn) + { + $this->connection = $connection; + $this->sql = $sql; + $this->fkColumn = $fkColumn; + } + + /** + * @return array> Rows, indexed by ID. + */ + private function load(): array + { + $results = $this->connection->fetchAll($this->sql); + + $data = []; + foreach ($results as $row) { + $data[$row[$this->fkColumn]][] = $row; + } + + return $data; + } + + /** + * Returns the DB row with the given ID. + * Loads all rows if necessary. + * Throws an exception if nothing found. + * + * @param string $id + * @return array + */ + public function get(string $id): array + { + if ($this->data === null) { + $this->data = $this->load(); + } + + if (!isset($this->data[$id])) { + return []; + } + return $this->data[$id]; + } +} diff --git a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php index 8291a595..073d1ac4 100644 --- a/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php +++ b/src/QueryFactory/SmartEagerLoad/Query/ManyToOnePartialQuery.php @@ -38,10 +38,9 @@ class ManyToOnePartialQuery implements PartialQuery public function __construct(PartialQuery $partialQuery, string $originTableName, string $tableName, string $pk, string $columnName) { - // TODO: move this in a separate function. The constructor is called for every bean. $this->partialQuery = $partialQuery; $this->mainTable = $tableName; - $this->key = $partialQuery->getKey().'__'.$columnName; + $this->key = $partialQuery->getKey().'__mto__'.$columnName; $this->pk = $pk; $this->originTableName = $originTableName; $this->columnName = $columnName; diff --git a/src/Utils/BeanDescriptor.php b/src/Utils/BeanDescriptor.php index e908259c..2200b3ae 100644 --- a/src/Utils/BeanDescriptor.php +++ b/src/Utils/BeanDescriptor.php @@ -1596,7 +1596,7 @@ private function generateGetForeignKeys(array $fks): MethodGenerator } return parent::getForeignKeys(\$tableName); EOF; - $code = sprintf($code, var_export($this->getTable()->getName(), true), $this->psr2VarExport($fkArray, ' ')); + $code = sprintf($code, var_export($this->getTable()->getName(), true), Psr2Utils::psr2VarExport($fkArray, ' ')); $method = new MethodGenerator('getForeignKeys'); $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED); @@ -1613,24 +1613,4 @@ private function generateGetForeignKeys(array $fks): MethodGenerator return $method; } - - /** - * @param mixed $var - * @param string $indent - * @return string - */ - private function psr2VarExport($var, string $indent=''): string - { - if (is_array($var)) { - $indexed = array_keys($var) === range(0, count($var) - 1); - $r = []; - foreach ($var as $key => $value) { - $r[] = "$indent " - . ($indexed ? '' : $this->psr2VarExport($key) . ' => ') - . $this->psr2VarExport($value, "$indent "); - } - return "[\n" . implode(",\n", $r) . "\n" . $indent . ']'; - } - return var_export($var, true); - } } diff --git a/src/Utils/DirectForeignKeyMethodDescriptor.php b/src/Utils/DirectForeignKeyMethodDescriptor.php index 97c88778..c2816a37 100644 --- a/src/Utils/DirectForeignKeyMethodDescriptor.php +++ b/src/Utils/DirectForeignKeyMethodDescriptor.php @@ -14,6 +14,7 @@ use Zend\Code\Generator\AbstractMemberGenerator; use Zend\Code\Generator\DocBlock\Tag\ReturnTag; use Zend\Code\Generator\MethodGenerator; +use function var_export; /** * Represents a method to get a list of beans from a direct foreign key pointing to our bean. @@ -133,10 +134,12 @@ public function getCode() : array $getter->setReturnType('?' . $classType); $code = sprintf( - 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s)->first();', + 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s, %s, %s)->first();', var_export($this->foreignKey->getLocalTableName(), true), var_export($tdbmFk->getCacheKey(), true), - $this->getFilters($this->foreignKey) + Psr2Utils::psr2InlineVarExport($this->foreignKey->getUnquotedLocalColumns()), + Psr2Utils::psr2InlineVarExport($this->foreignKey->getUnquotedForeignColumns()), + var_export($this->foreignKey->getForeignTableName(), true) ); } else { $getter->setDocBlock(sprintf('Returns the list of %s pointing to this bean via the %s column.', $beanClass, implode(', ', $this->foreignKey->getUnquotedLocalColumns()))); @@ -147,10 +150,12 @@ public function getCode() : array $getter->setReturnType(AlterableResultIterator::class); $code = sprintf( - 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s);', + 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s, %s, %s);', var_export($this->foreignKey->getLocalTableName(), true), var_export($tdbmFk->getCacheKey(), true), - $this->getFilters($this->foreignKey) + Psr2Utils::psr2InlineVarExport($this->foreignKey->getUnquotedLocalColumns()), + Psr2Utils::psr2InlineVarExport($this->foreignKey->getUnquotedForeignColumns()), + var_export($this->foreignKey->getForeignTableName(), true) ); } @@ -163,23 +168,6 @@ public function getCode() : array return [ $getter ]; } - private function getFilters(ForeignKeyConstraint $fk) : string - { - $counter = 0; - $parameters = []; - - $fkForeignColumns = $fk->getUnquotedForeignColumns(); - - foreach ($fk->getUnquotedLocalColumns() as $columnName) { - $fkColumn = $fkForeignColumns[$counter]; - $parameters[] = sprintf('%s => $this->get(%s, %s)', var_export($fk->getLocalTableName().'.'.$columnName, true), var_export($fkColumn, true), var_export($this->foreignKey->getForeignTableName(), true)); - ++$counter; - } - $parametersCode = '['.implode(', ', $parameters).']'; - - return $parametersCode; - } - private $hasLocalUniqueIndex; /** * Check if the ForeignKey have an unique index diff --git a/src/Utils/Psr2Utils.php b/src/Utils/Psr2Utils.php new file mode 100644 index 00000000..4e4766d2 --- /dev/null +++ b/src/Utils/Psr2Utils.php @@ -0,0 +1,52 @@ + $value) { + $r[] = "$indent " + . ($indexed ? '' : self::psr2VarExport($key) . ' => ') + . self::psr2VarExport($value, "$indent "); + } + return "[\n" . implode(",\n", $r) . "\n" . $indent . ']'; + } + return var_export($var, true); + } + + /** + * @param mixed $var + * @return string + */ + public static function psr2InlineVarExport($var): string + { + if (is_array($var)) { + $indexed = array_keys($var) === range(0, count($var) - 1); + $r = []; + foreach ($var as $key => $value) { + $r[] = ($indexed ? '' : self::psr2InlineVarExport($key) . ' => ') + . self::psr2InlineVarExport($value); + } + return '[' . implode(',', $r) . ']'; + } + return var_export($var, true); + } +} diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index 6bae87b5..6cde5a8b 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -21,6 +21,7 @@ namespace TheCodingMachine\TDBM; +use Author; use Doctrine\Common\Cache\ArrayCache; use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; @@ -40,6 +41,7 @@ use TheCodingMachine\TDBM\Test\Dao\AlbumDao; use TheCodingMachine\TDBM\Test\Dao\AllNullableDao; use TheCodingMachine\TDBM\Test\Dao\AnimalDao; +use TheCodingMachine\TDBM\Test\Dao\ArticleDao; use TheCodingMachine\TDBM\Test\Dao\ArtistDao; use TheCodingMachine\TDBM\Test\Dao\BaseObjectDao; use TheCodingMachine\TDBM\Test\Dao\Bean\AccountBean; @@ -2229,7 +2231,7 @@ public function testManyToOneEagerLoading(): void $countryIds[] = $user->getCountry()->getId(); } - $this->assertFalse($users->getIterator()->hasManyToOneDataLoader('__country_id')); + $this->assertFalse($users->getIterator()->hasManyToOneDataLoader('__mto__country_id')); $this->assertSame([2, 1, 3, 2, 2, 4], $countryIds); $countryNames = []; @@ -2237,10 +2239,26 @@ public function testManyToOneEagerLoading(): void $countryNames[] = $user->getCountry()->getLabel(); } - $this->assertTrue($users->getIterator()->hasManyToOneDataLoader('__country_id')); + $this->assertTrue($users->getIterator()->hasManyToOneDataLoader('__mto__country_id')); $this->assertSame(['UK', 'France', 'Jamaica', 'UK', 'UK', 'Mexico'], $countryNames); } + /** + * @depends testDaoGeneration + */ + public function testManyToOneEagerLoadingOnTableWithInheritance(): void + { + $articleDao = new ArticleDao($this->tdbmService); + /** @var ArticleBean[] $articles */ + $articles = $articleDao->findAll()->withOrder('id asc'); + $names = []; + foreach ($articles as $article) { + $names[] = $article->getAuthor()->getName(); + } + $this->assertCount(1, $names); + $this->assertSame('Bill Shakespeare', $names[0]); + } + /** * @depends testDaoGeneration */