Skip to content

Commit

Permalink
Optimizing ResultSet by having less loops.
Browse files Browse the repository at this point in the history
After this, I could trim 500ms off a loop having 1000 records. There
is still work to do, though :)
  • Loading branch information
lorenzo committed Mar 28, 2015
1 parent da61458 commit a69abed
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 70 deletions.
111 changes: 64 additions & 47 deletions src/ORM/ResultSet.php
Expand Up @@ -69,6 +69,13 @@ class ResultSet implements ResultSetInterface
*/
protected $_defaultTable;

/**
* The default table alias
*
* @var string
*/
protected $_defaultAlias;

/**
* List of associations that should be placed under the `_matchingData`
* result key.
Expand All @@ -88,9 +95,17 @@ class ResultSet implements ResultSetInterface
* Map of fields that are fetched from the statement with
* their type and the table they belong to
*
* @var string
* @var array
*/
protected $_map = [];

/**
* List of matching associations and the column keys to expect
* from each of them.
*
* @var array
*/
protected $_map;
protected $_matchingMapColumns = [];

/**
* Results that have been fetched or hydrated into the results.
Expand Down Expand Up @@ -152,6 +167,8 @@ public function __construct($query, $statement)
$this->_hydrate = $this->_query->hydrate();
$this->_entityClass = $repository->entityClass();
$this->_useBuffering = $query->bufferResults();
$this->_defaultAlias = $this->_defaultTable->alias();
$this->_calculateColumnMap();

if ($this->_useBuffering) {
$count = $this->count();
Expand Down Expand Up @@ -327,6 +344,7 @@ protected function _calculateAssociationMap()
$map = $this->_query->eagerLoader()->associationsMap($this->_defaultTable);
$this->_matchingMap = (new Collection($map))
->match(['matching' => true])
->indexBy('alias')
->toArray();

$this->_containMap = (new Collection(array_reverse($map)))
Expand All @@ -335,6 +353,32 @@ protected function _calculateAssociationMap()
->toArray();
}

/**
* Creates a map of row keys out of the query select clasuse that can be
* used to quickly hydrate correctly nested result sets.
*
* @return void
*/
protected function _calculateColumnMap()
{
$map = [];
foreach ($this->_query->clause('select') as $key => $field) {
if (strpos($key, '__') > 0) {
$parts = explode('__', $key, 2);
$map[$parts[0]][$key] = $parts[1];
} else {
$map[$this->_defaultAlias][$key] = $key;
}
}

foreach ($this->_matchingMap as $alias => $assoc) {
$this->_matchingMapColumns[$alias] = $map[$alias];
unset($map[$alias]);
}

$this->_map = $map;
}

/**
* Helper function to fetch the next result from the statement or
* seeded results.
Expand Down Expand Up @@ -362,7 +406,7 @@ protected function _fetchResult()
*/
protected function _groupResult($row)
{
$defaultAlias = $this->_defaultTable->alias();
$defaultAlias = $this->_defaultAlias;
$results = $presentAliases = [];
$options = [
'useSetters' => false,
Expand All @@ -371,57 +415,26 @@ protected function _groupResult($row)
'guard' => false
];

foreach ($this->_matchingMap as $matching) {
foreach ($row as $key => $value) {
if (strpos($key, $matching['alias'] . '__') !== 0) {
continue;
}
list($table, $field) = explode('__', $key);
$results['_matchingData'][$table][$field] = $value;

if (!isset($this->_containMap[$table])) {
unset($row[$key]);
}
}
if (empty($results['_matchingData'][$matching['alias']])) {
continue;
}

$results['_matchingData'][$matching['alias']] = $this->_castValues(
foreach ($this->_matchingMapColumns as $alias => $keys) {
$matching = $this->_matchingMap[$alias];
$results['_matchingData'][$alias] = $this->_castValues(
$matching['instance']->target(),
$results['_matchingData'][$matching['alias']]
array_combine(
$keys,
array_intersect_key($row, $keys)
)
);

if ($this->_hydrate) {
$options['source'] = $matching['alias'];
$entity = new $matching['entityClass']($results['_matchingData'][$matching['alias']], $options);
$options['source'] = $alias;
$entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
$entity->clean();
$results['_matchingData'][$matching['alias']] = $entity;
$results['_matchingData'][$alias] = $entity;
}
}

foreach ($row as $key => $value) {
$table = $defaultAlias;
$field = $key;

if ($value !== null && !is_scalar($value)) {
$results[$key] = $value;
continue;
}

if (empty($this->_map[$key])) {
$parts = explode('__', $key);
if (count($parts) > 1) {
$this->_map[$key] = $parts;
}
}

if (!empty($this->_map[$key])) {
list($table, $field) = $this->_map[$key];
}

foreach ($this->_map as $table => $keys) {
$results[$table] = array_combine($keys, array_intersect_key($row, $keys));
$presentAliases[$table] = true;
$results[$table][$field] = $value;
}

if (isset($presentAliases[$defaultAlias])) {
Expand All @@ -436,11 +449,15 @@ protected function _groupResult($row)
$alias = $assoc['nestKey'];
$instance = $assoc['instance'];

if (!isset($results[$alias])) {
if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
$results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
continue;
}

if (!$assoc['canBeJoined']) {
$results[$alias] = $row[$alias];
}

$target = $instance->target();
$options['source'] = $target->alias();
unset($presentAliases[$alias]);
Expand Down
28 changes: 6 additions & 22 deletions tests/TestCase/ORM/QueryTest.php
Expand Up @@ -1003,35 +1003,19 @@ public function testOverwriteMapReduce()
*/
public function testResultsAreWrappedInMapReduce()
{
$params = [$this->connection, $this->table];
$query = $this->getMock('\Cake\ORM\Query', ['execute'], $params);

$statement = $this->getMock(
'\Database\StatementInterface',
['fetch', 'closeCursor', 'rowCount']
);
$statement->expects($this->exactly(2))
->method('fetch')
->will($this->onConsecutiveCalls(['a' => 1], ['a' => 2]));

$statement->expects($this->once())
->method('rowCount')
->will($this->returnValue(2));

$query->expects($this->once())
->method('execute')
->will($this->returnValue($statement));

$table = TableRegistry::get('articles', ['table' => 'articles']);
$query = new Query($this->connection, $table);
$query->select(['a' => 'id'])->limit(2)->order(['id' => 'ASC']);
$query->mapReduce(function ($v, $k, $mr) {
$mr->emit($v['a']);
});
$query->mapReduce(
function ($v, $k, $mr) {
$mr->emitIntermediate($v, $k);
},
function ($v, $k, $mr) {
$mr->emit($v[0] + 1);
}
function ($v, $k, $mr) {
$mr->emit($v[0] + 1);
}
);

$this->assertEquals([2, 3], iterator_to_array($query->all()));
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCase/ORM/ResultSetTest.php
Expand Up @@ -328,7 +328,7 @@ public function testHasOneEagerLoaderLeavesEmptyAssocation()
public function testFetchMissingDefaultAlias()
{
$comments = TableRegistry::get('Comments');
$query = $comments->find();
$query = $comments->find()->select(['Other__field' => 'test']);
$query->autoFields(false);

$row = ['Other__field' => 'test'];
Expand Down

0 comments on commit a69abed

Please sign in to comment.