Skip to content

Commit 8e11aa2

Browse files
committed
Implementing deep association eager loading for secondary tables in
a query
1 parent 4aa7f7a commit 8e11aa2

File tree

2 files changed

+110
-24
lines changed

2 files changed

+110
-24
lines changed

lib/Cake/ORM/Query.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class Query extends DatabaseQuery {
2222

2323
protected $_aliasMap = [];
2424

25-
protected $_eagerLoading = false;
26-
2725
protected $_loadEagerly = [];
2826

2927
public function repository(Table $table = null) {
@@ -117,7 +115,7 @@ public function aliasFields($fields, $defaultAlias = null) {
117115
*/
118116
protected function _decorateResults($statement) {
119117
$statement = parent::_decorateResults($statement);
120-
if ($this->_eagerLoading) {
118+
if ($this->_loadEagerly) {
121119
if (!($statement instanceof BufferedStatement)) {
122120
$statement = new BufferedStatement($statement, $this->connection()->driver());
123121
}
@@ -140,7 +138,6 @@ protected function _transformQuery() {
140138
}
141139

142140
protected function _addContainments() {
143-
$this->_eagerLoading = false;
144141
$this->_loadEagerly = [];
145142
if (empty($this->_containments)) {
146143
return;
@@ -159,19 +156,25 @@ protected function _addContainments() {
159156
);
160157
}
161158

159+
foreach ($contain as $relation => $meta) {
160+
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
161+
$this->_loadEagerly[$relation] = $meta;
162+
}
163+
}
164+
162165
$firstLevelJoins = $this->_resolveFirstLevel($this->_table, $contain);
163166
foreach ($firstLevelJoins as $options) {
164167
$table = $options['association']->target();
165-
$this->_aliasMap[$table->alias()] = $table;
168+
$alias = $table->alias();
169+
$this->_aliasMap[$alias] = $table;
166170
$this->_addJoin($options['association'], $options['options']);
167-
}
168-
169-
foreach ($contain as $relation => $meta) {
170-
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
171-
$this->_eagerLoading = true;
172-
$this->_loadEagerly[$relation] = $meta;
171+
foreach ($options['associations'] as $relation => $meta) {
172+
if ($meta['instance'] && !$meta['instance']->canBeJoined()) {
173+
$this->_loadEagerly[$relation] = $meta;
174+
}
173175
}
174176
}
177+
175178
}
176179

177180
protected function _normalizeContain(Table $parent, $alias, $options) {
@@ -210,6 +213,7 @@ protected function _resolveFirstLevel($source, $associations) {
210213
if ($associated && $associated->canBeJoined()) {
211214
$result[$table] = [
212215
'association' => $associated,
216+
'associations' => $options['associations'],
213217
'options' => $options['config']
214218
];
215219
$result += $this->_resolveFirstLevel($associated->target(), $options['associations']);
@@ -224,18 +228,26 @@ protected function _addJoin($association, $options) {
224228
}
225229

226230
protected function _eagerLoad($statement) {
231+
$collectKeys = [];
232+
foreach ($this->_loadEagerly as $association => $meta) {
233+
$source = $meta['instance']->source();
234+
$alias = $source->alias();
235+
$pkField = key($this->aliasField($source->primaryKey(), $alias));
236+
$collectKeys[] = [$alias, $pkField];
237+
}
238+
227239
$keys = [];
228-
$alias = $this->_table->alias();
229-
$pkField = key($this->aliasField($this->_table->primaryKey()));
230240
while($result = $statement->fetch('assoc')) {
231-
$keys[$alias][] = $result[$pkField];
241+
foreach ($collectKeys as $parts) {
242+
$keys[$parts[0]][] = $result[$parts[1]];
243+
}
232244
}
233245

234246
$statement->rewind();
235247
foreach ($this->_loadEagerly as $association => $meta) {
236248
$contain = $meta['associations'];
237249
$f = $meta['instance']->eagerLoader(
238-
$keys[$alias],
250+
$keys[$meta['instance']->source()->alias()],
239251
$meta['config'] + compact('contain')
240252
);
241253
$statement = new CallbackStatement($statement, $this->connection()->driver(), $f);

lib/Cake/Test/TestCase/ORM/QueryTest.php

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public function setUp() {
6262
public function tearDown() {
6363
$this->connection->execute('DROP TABLE IF EXISTS articles');
6464
$this->connection->execute('DROP TABLE IF EXISTS authors');
65+
$this->connection->execute('DROP TABLE IF EXISTS publications');
6566
Table::clearRegistry();
6667
}
6768

@@ -70,24 +71,28 @@ public function tearDown() {
7071
*
7172
* @return void
7273
*/
73-
protected function _createAuthorsAndArticles() {
74+
protected function _createTables() {
7475
$table = 'CREATE TEMPORARY TABLE authors(id int, name varchar(50))';
7576
$this->connection->execute($table);
7677

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

81+
$table = 'CREATE TEMPORARY TABLE publications(id int, title varchar(20), body varchar(50), author_id int)';
82+
$this->connection->execute($table);
83+
8084
Table::config('authors', ['connection' => $this->connection]);
8185
Table::config('articles', ['connection' => $this->connection]);
86+
Table::config('publications', ['connection' => $this->connection]);
8287
}
8388

8489
/**
8590
* Auxiliary function to insert a couple rows in a newly created table
8691
*
8792
* @return void
8893
*/
89-
protected function _insertTwoRecords() {
90-
$this->_createAuthorsAndArticles();
94+
protected function _insertRecords() {
95+
$this->_createTables();
9196

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

104+
$data = ['id' => '2', 'title' => 'a publication', 'body' => 'a body', 'author_id' => 1];
105+
$result = $this->connection->insert(
106+
'publications',
107+
$data,
108+
['id' => 'integer', 'title' => 'string', 'body' => 'string', 'author_id' => 'integer']
109+
);
110+
111+
$result->bindValue(1, 3, 'integer');
112+
$result->bindValue(2, 'another publication');
113+
$result->bindValue(3, 'another body');
114+
$result->bindValue(4, 2);
115+
$result->execute();
116+
99117
$data = ['id' => '1', 'title' => 'a title', 'body' => 'a body', 'author_id' => 1];
100118
$result = $this->connection->insert(
101119
'articles',
@@ -272,7 +290,7 @@ public function testContainToFieldsDefault() {
272290
* @return void
273291
**/
274292
public function testContainResultFetchingOneLevel() {
275-
$this->_insertTwoRecords();
293+
$this->_insertRecords();
276294

277295
$query = new Query($this->connection);
278296
$table = Table::build('article', ['table' => 'articles']);
@@ -310,7 +328,7 @@ public function testContainResultFetchingOneLevel() {
310328
* @return void
311329
**/
312330
public function testHasManyEagerLoading() {
313-
$this->_insertTwoRecords();
331+
$this->_insertRecords();
314332

315333
$query = new Query($this->connection);
316334
$table = Table::build('author', ['connection' => $this->connection]);
@@ -361,7 +379,7 @@ public function testHasManyEagerLoading() {
361379
* @return void
362380
**/
363381
public function testHasManyEagerLoadingFields() {
364-
$this->_insertTwoRecords();
382+
$this->_insertRecords();
365383

366384
$query = new Query($this->connection);
367385
$table = Table::build('author', ['connection' => $this->connection]);
@@ -397,7 +415,7 @@ public function testHasManyEagerLoadingFields() {
397415
* @return void
398416
**/
399417
public function testHasManyEagerLoadingOrder() {
400-
$statement = $this->_insertTwoRecords();
418+
$statement = $this->_insertRecords();
401419
$statement->bindValue(1, 3, 'integer');
402420
$statement->bindValue(2, 'a fine title');
403421
$statement->bindValue(3, 'a fine body');
@@ -439,12 +457,12 @@ public function testHasManyEagerLoadingOrder() {
439457
}
440458

441459
/**
442-
* Tests that deep associations can be eagerly laoded
460+
* Tests that deep associations can be eagerly loaded
443461
*
444462
* @return void
445463
**/
446464
public function testHasManyEagerLoadingDeep() {
447-
$this->_insertTwoRecords();
465+
$this->_insertRecords();
448466

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

504+
/**
505+
* Tests that hasMany associations can be loaded even when related to a secondary
506+
* model in the query
507+
*
508+
* @return void
509+
**/
510+
public function testHasManyEagerLoadingFromSecondaryTable() {
511+
$this->_insertRecords();
512+
513+
$query = new Query($this->connection);
514+
$author = Table::build('author', ['connection' => $this->connection]);
515+
$article = Table::build('article', ['connection' => $this->connection]);
516+
$publication = Table::build('publication', ['connection' => $this->connection]);
517+
518+
$author->hasMany('publication', ['property' => 'publications']);
519+
$article->belongsTo('author');
520+
521+
$results = $query->repository($article)
522+
->select()
523+
->contain(['author' => ['publication']])
524+
->toArray();
525+
$expected = [
526+
[
527+
'id' => 1,
528+
'title' => 'a title',
529+
'body' => 'a body',
530+
'author_id' => 1,
531+
'author' => [
532+
'id' => 1, 'name' => 'Chuck Norris',
533+
'publications' => [
534+
[
535+
'id' => '2', 'title' => 'a publication',
536+
'body' => 'a body', 'author_id' => 1
537+
]
538+
]
539+
]
540+
],
541+
[
542+
'id' => 2,
543+
'title' => 'another title',
544+
'body' => 'another body',
545+
'author_id' => 2,
546+
'author' => [
547+
'id' => 2, 'name' => 'Bruce Lee',
548+
'publications' => [
549+
[
550+
'id' => 3, 'title' => 'another publication',
551+
'body' => 'another body', 'author_id' => 2
552+
]
553+
]
554+
]
555+
]
556+
];
557+
$this->assertEquals($expected, $results);
558+
}
559+
486560
}

0 commit comments

Comments
 (0)