Skip to content

Commit

Permalink
Implementing deep association eager loading for secondary tables in
Browse files Browse the repository at this point in the history
a query
  • Loading branch information
lorenzo committed May 20, 2013
1 parent 4aa7f7a commit 8e11aa2
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 24 deletions.
42 changes: 27 additions & 15 deletions lib/Cake/ORM/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ class Query extends DatabaseQuery {

protected $_aliasMap = [];

protected $_eagerLoading = false;

protected $_loadEagerly = [];

public function repository(Table $table = null) {
Expand Down Expand Up @@ -117,7 +115,7 @@ public function aliasFields($fields, $defaultAlias = null) {
*/
protected function _decorateResults($statement) {
$statement = parent::_decorateResults($statement);
if ($this->_eagerLoading) {
if ($this->_loadEagerly) {
if (!($statement instanceof BufferedStatement)) {
$statement = new BufferedStatement($statement, $this->connection()->driver());
}
Expand All @@ -140,7 +138,6 @@ protected function _transformQuery() {
}

protected function _addContainments() {
$this->_eagerLoading = false;
$this->_loadEagerly = [];
if (empty($this->_containments)) {
return;
Expand All @@ -159,19 +156,25 @@ protected function _addContainments() {
);
}

foreach ($contain as $relation => $meta) {
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
$this->_loadEagerly[$relation] = $meta;
}
}

$firstLevelJoins = $this->_resolveFirstLevel($this->_table, $contain);
foreach ($firstLevelJoins as $options) {
$table = $options['association']->target();
$this->_aliasMap[$table->alias()] = $table;
$alias = $table->alias();
$this->_aliasMap[$alias] = $table;
$this->_addJoin($options['association'], $options['options']);
}

foreach ($contain as $relation => $meta) {
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
$this->_eagerLoading = true;
$this->_loadEagerly[$relation] = $meta;
foreach ($options['associations'] as $relation => $meta) {
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
$this->_loadEagerly[$relation] = $meta;
}
}
}

}

protected function _normalizeContain(Table $parent, $alias, $options) {
Expand Down Expand Up @@ -210,6 +213,7 @@ protected function _resolveFirstLevel($source, $associations) {
if ($associated && $associated->canBeJoined()) {
$result[$table] = [
'association' => $associated,
'associations' => $options['associations'],
'options' => $options['config']
];
$result += $this->_resolveFirstLevel($associated->target(), $options['associations']);
Expand All @@ -224,18 +228,26 @@ protected function _addJoin($association, $options) {
}

protected function _eagerLoad($statement) {
$collectKeys = [];
foreach ($this->_loadEagerly as $association => $meta) {
$source = $meta['instance']->source();
$alias = $source->alias();
$pkField = key($this->aliasField($source->primaryKey(), $alias));
$collectKeys[] = [$alias, $pkField];
}

$keys = [];
$alias = $this->_table->alias();
$pkField = key($this->aliasField($this->_table->primaryKey()));
while($result = $statement->fetch('assoc')) {
$keys[$alias][] = $result[$pkField];
foreach ($collectKeys as $parts) {
$keys[$parts[0]][] = $result[$parts[1]];
}
}

$statement->rewind();
foreach ($this->_loadEagerly as $association => $meta) {
$contain = $meta['associations'];
$f = $meta['instance']->eagerLoader(
$keys[$alias],
$keys[$meta['instance']->source()->alias()],
$meta['config'] + compact('contain')
);
$statement = new CallbackStatement($statement, $this->connection()->driver(), $f);
Expand Down
92 changes: 83 additions & 9 deletions lib/Cake/Test/TestCase/ORM/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function setUp() {
public function tearDown() {
$this->connection->execute('DROP TABLE IF EXISTS articles');
$this->connection->execute('DROP TABLE IF EXISTS authors');
$this->connection->execute('DROP TABLE IF EXISTS publications');
Table::clearRegistry();
}

Expand All @@ -70,24 +71,28 @@ public function tearDown() {
*
* @return void
*/
protected function _createAuthorsAndArticles() {
protected function _createTables() {
$table = 'CREATE TEMPORARY TABLE authors(id int, name varchar(50))';
$this->connection->execute($table);

$table = 'CREATE TEMPORARY TABLE articles(id int, title varchar(20), body varchar(50), author_id int)';
$this->connection->execute($table);

$table = 'CREATE TEMPORARY TABLE publications(id int, title varchar(20), body varchar(50), author_id int)';
$this->connection->execute($table);

Table::config('authors', ['connection' => $this->connection]);
Table::config('articles', ['connection' => $this->connection]);
Table::config('publications', ['connection' => $this->connection]);
}

/**
* Auxiliary function to insert a couple rows in a newly created table
*
* @return void
*/
protected function _insertTwoRecords() {
$this->_createAuthorsAndArticles();
protected function _insertRecords() {
$this->_createTables();

$data = ['id' => '1', 'name' => 'Chuck Norris'];
$result = $this->connection->insert('authors', $data, ['id' => 'integer', 'name' => 'string']);
Expand All @@ -96,6 +101,19 @@ protected function _insertTwoRecords() {
$result->bindValue(2, 'Bruce Lee');
$result->execute();

$data = ['id' => '2', 'title' => 'a publication', 'body' => 'a body', 'author_id' => 1];
$result = $this->connection->insert(
'publications',
$data,
['id' => 'integer', 'title' => 'string', 'body' => 'string', 'author_id' => 'integer']
);

$result->bindValue(1, 3, 'integer');
$result->bindValue(2, 'another publication');
$result->bindValue(3, 'another body');
$result->bindValue(4, 2);
$result->execute();

$data = ['id' => '1', 'title' => 'a title', 'body' => 'a body', 'author_id' => 1];
$result = $this->connection->insert(
'articles',
Expand Down Expand Up @@ -272,7 +290,7 @@ public function testContainToFieldsDefault() {
* @return void
**/
public function testContainResultFetchingOneLevel() {
$this->_insertTwoRecords();
$this->_insertRecords();

$query = new Query($this->connection);
$table = Table::build('article', ['table' => 'articles']);
Expand Down Expand Up @@ -310,7 +328,7 @@ public function testContainResultFetchingOneLevel() {
* @return void
**/
public function testHasManyEagerLoading() {
$this->_insertTwoRecords();
$this->_insertRecords();

$query = new Query($this->connection);
$table = Table::build('author', ['connection' => $this->connection]);
Expand Down Expand Up @@ -361,7 +379,7 @@ public function testHasManyEagerLoading() {
* @return void
**/
public function testHasManyEagerLoadingFields() {
$this->_insertTwoRecords();
$this->_insertRecords();

$query = new Query($this->connection);
$table = Table::build('author', ['connection' => $this->connection]);
Expand Down Expand Up @@ -397,7 +415,7 @@ public function testHasManyEagerLoadingFields() {
* @return void
**/
public function testHasManyEagerLoadingOrder() {
$statement = $this->_insertTwoRecords();
$statement = $this->_insertRecords();
$statement->bindValue(1, 3, 'integer');
$statement->bindValue(2, 'a fine title');
$statement->bindValue(3, 'a fine body');
Expand Down Expand Up @@ -439,12 +457,12 @@ public function testHasManyEagerLoadingOrder() {
}

/**
* Tests that deep associations can be eagerly laoded
* Tests that deep associations can be eagerly loaded
*
* @return void
**/
public function testHasManyEagerLoadingDeep() {
$this->_insertTwoRecords();
$this->_insertRecords();

$query = new Query($this->connection);
$table = Table::build('author', ['connection' => $this->connection]);
Expand Down Expand Up @@ -483,4 +501,60 @@ public function testHasManyEagerLoadingDeep() {
$this->assertEquals($expected, $results);
}

/**
* Tests that hasMany associations can be loaded even when related to a secondary
* model in the query
*
* @return void
**/
public function testHasManyEagerLoadingFromSecondaryTable() {
$this->_insertRecords();

$query = new Query($this->connection);
$author = Table::build('author', ['connection' => $this->connection]);
$article = Table::build('article', ['connection' => $this->connection]);
$publication = Table::build('publication', ['connection' => $this->connection]);

$author->hasMany('publication', ['property' => 'publications']);
$article->belongsTo('author');

$results = $query->repository($article)
->select()
->contain(['author' => ['publication']])
->toArray();
$expected = [
[
'id' => 1,
'title' => 'a title',
'body' => 'a body',
'author_id' => 1,
'author' => [
'id' => 1, 'name' => 'Chuck Norris',
'publications' => [
[
'id' => '2', 'title' => 'a publication',
'body' => 'a body', 'author_id' => 1
]
]
]
],
[
'id' => 2,
'title' => 'another title',
'body' => 'another body',
'author_id' => 2,
'author' => [
'id' => 2, 'name' => 'Bruce Lee',
'publications' => [
[
'id' => 3, 'title' => 'another publication',
'body' => 'another body', 'author_id' => 2
]
]
]
]
];
$this->assertEquals($expected, $results);
}

}

0 comments on commit 8e11aa2

Please sign in to comment.