mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
FIX Empty relations don't have extra DB calls with eager-loading (#10886)
This commit is contained in:
parent
ae49e134a9
commit
3628cec1f3
@ -1007,10 +1007,15 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
}
|
}
|
||||||
$belongsToComponent = $schema->belongsToComponent($parentDataClass, $relationName);
|
$belongsToComponent = $schema->belongsToComponent($parentDataClass, $relationName);
|
||||||
if ($belongsToComponent) {
|
if ($belongsToComponent) {
|
||||||
|
$joinField = $schema->getRemoteJoinField($parentDataClass, $relationName, 'belongs_to', $polymorphic);
|
||||||
return [
|
return [
|
||||||
$belongsToComponent,
|
$belongsToComponent,
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
$schema->getRemoteJoinField($parentDataClass, $relationName, 'belongs_to'),
|
[
|
||||||
|
'joinField' => $joinField,
|
||||||
|
'polymorphic' => $polymorphic,
|
||||||
|
'parentClass' => $parentDataClass
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$hasManyComponent = $schema->hasManyComponent($parentDataClass, $relationName);
|
$hasManyComponent = $schema->hasManyComponent($parentDataClass, $relationName);
|
||||||
@ -1061,7 +1066,6 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
foreach ($this->eagerLoadRelationChains as $relationChain) {
|
foreach ($this->eagerLoadRelationChains as $relationChain) {
|
||||||
$parentDataClass = $this->dataClass();
|
$parentDataClass = $this->dataClass();
|
||||||
$parentIDs = $topLevelIDs;
|
$parentIDs = $topLevelIDs;
|
||||||
$parentRelationName = '';
|
|
||||||
/** @var Query|array<DataObject|EagerLoadedList> */
|
/** @var Query|array<DataObject|EagerLoadedList> */
|
||||||
$parentRelationData = $query;
|
$parentRelationData = $query;
|
||||||
$chainToDate = [];
|
$chainToDate = [];
|
||||||
@ -1080,7 +1084,8 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$relationComponent,
|
$relationComponent,
|
||||||
$relationDataClass,
|
$relationDataClass,
|
||||||
implode('.', $chainToDate),
|
implode('.', $chainToDate),
|
||||||
$relationName
|
$relationName,
|
||||||
|
$relationType
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'belongs_to':
|
case 'belongs_to':
|
||||||
@ -1090,7 +1095,8 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$relationComponent,
|
$relationComponent,
|
||||||
$relationDataClass,
|
$relationDataClass,
|
||||||
implode('.', $chainToDate),
|
implode('.', $chainToDate),
|
||||||
$relationName
|
$relationName,
|
||||||
|
$relationType
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'has_many':
|
case 'has_many':
|
||||||
@ -1101,7 +1107,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$relationDataClass,
|
$relationDataClass,
|
||||||
implode('.', $chainToDate),
|
implode('.', $chainToDate),
|
||||||
$relationName,
|
$relationName,
|
||||||
$parentRelationName
|
$relationType
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'many_many':
|
case 'many_many':
|
||||||
@ -1112,15 +1118,14 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$relationDataClass,
|
$relationDataClass,
|
||||||
implode('.', $chainToDate),
|
implode('.', $chainToDate),
|
||||||
$relationName,
|
$relationName,
|
||||||
$parentRelationName,
|
$parentDataClass,
|
||||||
$parentDataClass
|
$relationType
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new LogicException("Unexpected relation type $relationType");
|
throw new LogicException("Unexpected relation type $relationType");
|
||||||
}
|
}
|
||||||
$parentDataClass = $relationDataClass;
|
$parentDataClass = $relationDataClass;
|
||||||
$parentRelationName = $relationName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1130,7 +1135,8 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
string $hasOneIDField,
|
string $hasOneIDField,
|
||||||
string $relationDataClass,
|
string $relationDataClass,
|
||||||
string $relationChain,
|
string $relationChain,
|
||||||
string $relationName
|
string $relationName,
|
||||||
|
string $relationType
|
||||||
): array {
|
): array {
|
||||||
$fetchedIDs = [];
|
$fetchedIDs = [];
|
||||||
$addTo = [];
|
$addTo = [];
|
||||||
@ -1155,7 +1161,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$addTo[$hasOneID] = ['ID' => $parentRow['ID'], 'list' => $parentData];
|
$addTo[$hasOneID] = ['ID' => $parentRow['ID'], 'list' => $parentData];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new LogicException("Invalid parent for eager loading has_one relation $relationName");
|
throw new LogicException("Invalid parent for eager loading $relationType relation $relationName");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1179,34 +1185,54 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$added) {
|
if (!$added) {
|
||||||
throw new LogicException("Couldn't find parent for record $fetchedID on has_one relation $relationName");
|
throw new LogicException("Couldn't find parent for record $fetchedID on $relationType relation $relationName");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Unlike the other relation types, we don't have to explicitly fill empty DataObject records
|
||||||
|
// into the has_one components - DataObject does that for us in getComponent() without any extra
|
||||||
|
// db calls.
|
||||||
|
|
||||||
return [$fetchedRecords, $fetchedIDs];
|
return [$fetchedRecords, $fetchedIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchEagerLoadBelongsTo(
|
private function fetchEagerLoadBelongsTo(
|
||||||
Query|array $parents,
|
Query|array $parents,
|
||||||
array $parentIDs,
|
array $parentIDs,
|
||||||
string $belongsToIDField,
|
array $component,
|
||||||
string $relationDataClass,
|
string $relationDataClass,
|
||||||
string $relationChain,
|
string $relationChain,
|
||||||
string $relationName
|
string $relationName,
|
||||||
|
string $relationType
|
||||||
): array {
|
): array {
|
||||||
|
$belongsToIDField = $component['joinField'];
|
||||||
// Get ALL of the items for this relation up front, for ALL of the parents
|
// Get ALL of the items for this relation up front, for ALL of the parents
|
||||||
// Fetched as an array to avoid sporadic additional queries when the DataList is looped directly
|
// Fetched as an array to avoid sporadic additional queries when the DataList is looped directly
|
||||||
$fetchedRecords = DataObject::get($relationDataClass)->filter([$belongsToIDField => $parentIDs])->toArray();
|
$fetchedRecords = DataObject::get($relationDataClass)->filter([$belongsToIDField => $parentIDs])->toArray();
|
||||||
$fetchedIDs = [];
|
$fetchedIDs = [];
|
||||||
|
$foundParentIDs = [];
|
||||||
|
|
||||||
// Add fetched record to the correct place
|
// Add fetched record to the correct place
|
||||||
foreach ($fetchedRecords as $fetched) {
|
foreach ($fetchedRecords as $fetched) {
|
||||||
$fetchedIDs[] = $fetched->ID;
|
$fetchedIDs[] = $fetched->ID;
|
||||||
$parentID = $fetched->$belongsToIDField;
|
$parentID = $fetched->$belongsToIDField;
|
||||||
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $fetched, 'has_one');
|
$foundParentIDs[] = $parentID;
|
||||||
|
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $fetched, $relationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load empty DataObject records into any parents which have no child records
|
||||||
|
$missingParentIDs = array_diff($parentIDs, $foundParentIDs);
|
||||||
|
$this->fillEmptyEagerLoadedRelations(
|
||||||
|
$parents,
|
||||||
|
$missingParentIDs,
|
||||||
|
$relationChain,
|
||||||
|
$relationName,
|
||||||
|
$relationType,
|
||||||
|
$relationDataClass,
|
||||||
|
null,
|
||||||
|
$component
|
||||||
|
);
|
||||||
|
|
||||||
return [$fetchedRecords, $fetchedIDs];
|
return [$fetchedRecords, $fetchedIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1216,7 +1242,8 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
string $hasManyIDField,
|
string $hasManyIDField,
|
||||||
string $relationDataClass,
|
string $relationDataClass,
|
||||||
string $relationChain,
|
string $relationChain,
|
||||||
string $relationName
|
string $relationName,
|
||||||
|
string $relationType
|
||||||
): array {
|
): array {
|
||||||
// Get ALL of the items for this relation up front, for ALL of the parents
|
// Get ALL of the items for this relation up front, for ALL of the parents
|
||||||
// Fetched as an array to avoid sporadic additional queries when the DataList is looped directly
|
// Fetched as an array to avoid sporadic additional queries when the DataList is looped directly
|
||||||
@ -1234,12 +1261,24 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
// If we haven't created a list yet, create it and add it to the correct parent list/record
|
// If we haven't created a list yet, create it and add it to the correct parent list/record
|
||||||
$eagerLoadedList = EagerLoadedList::create($relationDataClass, HasManyList::class, $parentID);
|
$eagerLoadedList = EagerLoadedList::create($relationDataClass, HasManyList::class, $parentID);
|
||||||
$eagerLoadedLists[$parentID] = $eagerLoadedList;
|
$eagerLoadedLists[$parentID] = $eagerLoadedList;
|
||||||
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $eagerLoadedList, 'has_many');
|
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $eagerLoadedList, $relationType);
|
||||||
}
|
}
|
||||||
// Add this row to the list
|
// Add this row to the list
|
||||||
$eagerLoadedList->addRow($row);
|
$eagerLoadedList->addRow($row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load empty EagerLoadedLists into any parents which have no child records
|
||||||
|
$missingParentIDs = array_diff($parentIDs, array_keys($eagerLoadedLists));
|
||||||
|
$this->fillEmptyEagerLoadedRelations(
|
||||||
|
$parents,
|
||||||
|
$missingParentIDs,
|
||||||
|
$relationChain,
|
||||||
|
$relationName,
|
||||||
|
$relationType,
|
||||||
|
$relationDataClass,
|
||||||
|
HasManyList::class
|
||||||
|
);
|
||||||
|
|
||||||
return [$eagerLoadedLists, $fetchedIDs];
|
return [$eagerLoadedLists, $fetchedIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1250,7 +1289,8 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
string $relationDataClass,
|
string $relationDataClass,
|
||||||
string $relationChain,
|
string $relationChain,
|
||||||
string $relationName,
|
string $relationName,
|
||||||
string $parentDataClass
|
string $parentDataClass,
|
||||||
|
string $relationType
|
||||||
): array {
|
): array {
|
||||||
$parentIDField = $manyManyLastComponent['parentField'];
|
$parentIDField = $manyManyLastComponent['parentField'];
|
||||||
$childIDField = $manyManyLastComponent['childField'];
|
$childIDField = $manyManyLastComponent['childField'];
|
||||||
@ -1288,6 +1328,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$extraFields
|
$extraFields
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
$relationListClass = get_class($relationList);
|
||||||
|
|
||||||
// Get ALL of the items for this relation up front, for ALL of the parents
|
// Get ALL of the items for this relation up front, for ALL of the parents
|
||||||
$fetchedRows = $relationList->forForeignID($parentIDs)->getFinalisedQuery();
|
$fetchedRows = $relationList->forForeignID($parentIDs)->getFinalisedQuery();
|
||||||
@ -1307,14 +1348,27 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$eagerLoadedList = $eagerLoadedLists[$parentID];
|
$eagerLoadedList = $eagerLoadedLists[$parentID];
|
||||||
} else {
|
} else {
|
||||||
// If we haven't created a list yet, create it and add it to the correct parent list/record
|
// If we haven't created a list yet, create it and add it to the correct parent list/record
|
||||||
$eagerLoadedList = EagerLoadedList::create($relationDataClass, get_class($relationList), $parentID, $manyManyLastComponent);
|
$eagerLoadedList = EagerLoadedList::create($relationDataClass, $relationListClass, $parentID, $manyManyLastComponent);
|
||||||
$eagerLoadedLists[$parentID] = $eagerLoadedList;
|
$eagerLoadedLists[$parentID] = $eagerLoadedList;
|
||||||
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $eagerLoadedList, 'many_many');
|
$this->addEagerLoadedDataToParent($parents, $parentID, $relationChain, $relationName, $eagerLoadedList, $relationType);
|
||||||
}
|
}
|
||||||
// Add this row to the list
|
// Add this row to the list
|
||||||
$eagerLoadedList->addRow($relationItem);
|
$eagerLoadedList->addRow($relationItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load empty EagerLoadedLists into any parents which have no child records
|
||||||
|
$missingParentIDs = array_diff($parentIDs, array_keys($eagerLoadedLists));
|
||||||
|
$this->fillEmptyEagerLoadedRelations(
|
||||||
|
$parents,
|
||||||
|
$missingParentIDs,
|
||||||
|
$relationChain,
|
||||||
|
$relationName,
|
||||||
|
$relationType,
|
||||||
|
$relationDataClass,
|
||||||
|
$relationListClass,
|
||||||
|
$manyManyLastComponent
|
||||||
|
);
|
||||||
|
|
||||||
return [$eagerLoadedLists, $fetchedIDs];
|
return [$eagerLoadedLists, $fetchedIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1367,6 +1421,46 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function fillEmptyEagerLoadedRelations(
|
||||||
|
Query|array $parents,
|
||||||
|
array $missingParentIDs,
|
||||||
|
string $relationChain,
|
||||||
|
string $relationName,
|
||||||
|
string $relationType,
|
||||||
|
string $relationDataClass,
|
||||||
|
?string $relationListClass = null,
|
||||||
|
?array $component = null
|
||||||
|
): void {
|
||||||
|
foreach ($missingParentIDs as $id) {
|
||||||
|
// Build the empty list or record
|
||||||
|
switch ($relationType) {
|
||||||
|
case 'has_one':
|
||||||
|
$dummyData = Injector::inst()->create($relationDataClass);
|
||||||
|
break;
|
||||||
|
case 'belongs_to':
|
||||||
|
$dummyData = Injector::inst()->create($relationDataClass);
|
||||||
|
$joinField = $component['joinField'];
|
||||||
|
if ($component['polymorphic']) {
|
||||||
|
$dummyData->{$joinField . 'ID'} = $id;
|
||||||
|
$dummyData->{$joinField . 'Class'} = $component['parentClass'];
|
||||||
|
} else {
|
||||||
|
$dummyData->$joinField = $id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'has_many':
|
||||||
|
$dummyData = EagerLoadedList::create($relationDataClass, $relationListClass, $id);
|
||||||
|
break;
|
||||||
|
case 'many_many':
|
||||||
|
$dummyData = EagerLoadedList::create($relationDataClass, $relationListClass, $id, $component);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new LogicException("Unexpected relation type $relationType");
|
||||||
|
}
|
||||||
|
// Add the empty list or record to this parent
|
||||||
|
$this->addEagerLoadedDataToParent($parents, $id, $relationChain, $relationName, $dummyData, $relationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eager load relations for DataObjects in this DataList including nested relations
|
* Eager load relations for DataObjects in this DataList including nested relations
|
||||||
*
|
*
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace SilverStripe\ORM\Tests;
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use LogicException;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\ORM\DataList;
|
use SilverStripe\ORM\DataList;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
@ -37,9 +38,12 @@ use SilverStripe\ORM\Tests\DataListTest\EagerLoading\MixedManyManyEagerLoadObjec
|
|||||||
|
|
||||||
class DataListEagerLoadingTest extends SapphireTest
|
class DataListEagerLoadingTest extends SapphireTest
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $usesDatabase = true;
|
protected $usesDatabase = true;
|
||||||
|
|
||||||
|
private const SHOW_QUERIES_RESET = 'SET_TO_THIS_VALUE_WHEN_FINISHED';
|
||||||
|
|
||||||
|
private $showQueries = self::SHOW_QUERIES_RESET;
|
||||||
|
|
||||||
public static function getExtraDataObjects()
|
public static function getExtraDataObjects()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -100,6 +104,51 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start counting the number of SELECT database queries being run
|
||||||
|
*/
|
||||||
|
private function startCountingSelectQueries(): void
|
||||||
|
{
|
||||||
|
if ($this->showQueries !== self::SHOW_QUERIES_RESET) {
|
||||||
|
throw new LogicException('showQueries wasnt reset, you did something wrong');
|
||||||
|
}
|
||||||
|
$this->showQueries = $_REQUEST['showqueries'] ?? null;
|
||||||
|
// force showqueries on to count the number of SELECT statements via output-buffering
|
||||||
|
// if this approach turns out to be too brittle later on, switch to what debugbar
|
||||||
|
// does and use tractorcow/proxy-db which should be installed as a dev-dependency
|
||||||
|
// https://github.com/lekoala/silverstripe-debugbar/blob/master/code/Collector/DatabaseCollector.php#L79
|
||||||
|
$_REQUEST['showqueries'] = 1;
|
||||||
|
ob_start();
|
||||||
|
echo '__START_ITERATE__';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop counting database queries and return the count
|
||||||
|
*/
|
||||||
|
private function stopCountingSelectQueries(): int
|
||||||
|
{
|
||||||
|
$s = ob_get_clean();
|
||||||
|
$s = preg_replace('/.*__START_ITERATE__/s', '', $s);
|
||||||
|
$this->resetShowQueries();
|
||||||
|
return substr_count($s, ': SELECT');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the "showqueries" request var
|
||||||
|
*/
|
||||||
|
private function resetShowQueries(): void
|
||||||
|
{
|
||||||
|
if ($this->showQueries === self::SHOW_QUERIES_RESET) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($this->showQueries) {
|
||||||
|
$_REQUEST['showqueries'] = $this->showQueries;
|
||||||
|
} else {
|
||||||
|
unset($_REQUEST['showqueries']);
|
||||||
|
}
|
||||||
|
$this->showQueries = self::SHOW_QUERIES_RESET;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider provideEagerLoadRelations
|
* @dataProvider provideEagerLoadRelations
|
||||||
*/
|
*/
|
||||||
@ -615,17 +664,9 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
{
|
{
|
||||||
$results = [];
|
$results = [];
|
||||||
$selectCount = -1;
|
$selectCount = -1;
|
||||||
$showqueries = $_REQUEST['showqueries'] ?? null;
|
|
||||||
try {
|
try {
|
||||||
// force showqueries on to count the number of SELECT statements via output-buffering
|
$this->startCountingSelectQueries();
|
||||||
// if this approach turns out to be too brittle later on, switch to what debugbar
|
|
||||||
// does and use tractorcow/proxy-db which should be installed as a dev-dependency
|
|
||||||
// https://github.com/lekoala/silverstripe-debugbar/blob/master/code/Collector/DatabaseCollector.php#L79
|
|
||||||
$_REQUEST['showqueries'] = 1;
|
|
||||||
ob_start();
|
|
||||||
echo '__START_ITERATE__';
|
|
||||||
$results = [];
|
$results = [];
|
||||||
$i = 0;
|
|
||||||
if ($chunks) {
|
if ($chunks) {
|
||||||
$dataList = $dataList->chunkedFetch($chunks);
|
$dataList = $dataList->chunkedFetch($chunks);
|
||||||
}
|
}
|
||||||
@ -695,15 +736,9 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$s = ob_get_clean();
|
$selectCount = $this->stopCountingSelectQueries();
|
||||||
$s = preg_replace('/.*__START_ITERATE__/s', '', $s);
|
|
||||||
$selectCount = substr_count($s, ': SELECT');
|
|
||||||
} finally {
|
} finally {
|
||||||
if ($showqueries) {
|
$this->resetShowQueries();
|
||||||
$_REQUEST['showqueries'] = $showqueries;
|
|
||||||
} else {
|
|
||||||
unset($_REQUEST['showqueries']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return [$results, $selectCount];
|
return [$results, $selectCount];
|
||||||
}
|
}
|
||||||
@ -994,11 +1029,11 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider provideEagerLoadingEmptyRelationLists
|
* @dataProvider provideEagerLoadingEmptyRelations
|
||||||
*/
|
*/
|
||||||
public function testEagerLoadingEmptyRelationLists(string $iden, string $eagerLoad): void
|
public function testEagerLoadingEmptyRelations(string $iden, string $eagerLoad): void
|
||||||
{
|
{
|
||||||
$numBaseRecords = 2;
|
$numBaseRecords = 3;
|
||||||
$numLevel1Records = 2;
|
$numLevel1Records = 2;
|
||||||
$numLevel2Records = 2;
|
$numLevel2Records = 2;
|
||||||
// Similar to createEagerLoadData(), except with less relations and
|
// Similar to createEagerLoadData(), except with less relations and
|
||||||
@ -1008,9 +1043,34 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
$obj = new EagerLoadObject();
|
$obj = new EagerLoadObject();
|
||||||
$obj->Title = "obj $i";
|
$obj->Title = "obj $i";
|
||||||
$objID = $obj->write();
|
$objID = $obj->write();
|
||||||
|
if ($i > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// has_one - level1
|
||||||
|
$hasOneObj = new HasOneEagerLoadObject();
|
||||||
|
$hasOneObj->Title = "hasOneObj $i";
|
||||||
|
$hasOneObjID = $hasOneObj->write();
|
||||||
|
$obj->HasOneEagerLoadObjectID = $hasOneObjID;
|
||||||
|
$obj->write();
|
||||||
|
// belongs_to - level1
|
||||||
|
$belongsToObj = new BelongsToEagerLoadObject();
|
||||||
|
$belongsToObj->EagerLoadObjectID = $objID;
|
||||||
|
$belongsToObj->Title = "belongsToObj $i";
|
||||||
|
$belongsToObjID = $belongsToObj->write();
|
||||||
if ($i > 0) {
|
if ($i > 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// has_one - level2
|
||||||
|
$hasOneSubObj = new HasOneSubEagerLoadObject();
|
||||||
|
$hasOneSubObj->Title = "hasOneSubObj $i";
|
||||||
|
$hasOneSubObjID = $hasOneSubObj->write();
|
||||||
|
$hasOneObj->HasOneSubEagerLoadObjectID = $hasOneSubObjID;
|
||||||
|
$hasOneObj->write();
|
||||||
|
// belongs_to - level2
|
||||||
|
$belongsToSubObj = new BelongsToSubEagerLoadObject();
|
||||||
|
$belongsToSubObj->BelongsToEagerLoadObjectID = $belongsToObjID;
|
||||||
|
$belongsToSubObj->Title = "belongsToSubObj $i";
|
||||||
|
$belongsToSubObj->write();
|
||||||
// has_many
|
// has_many
|
||||||
for ($j = 0; $j < $numLevel1Records; $j++) {
|
for ($j = 0; $j < $numLevel1Records; $j++) {
|
||||||
$hasManyObj = new HasManyEagerLoadObject();
|
$hasManyObj = new HasManyEagerLoadObject();
|
||||||
@ -1065,31 +1125,81 @@ class DataListEagerLoadingTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The actual test starts here - everything above is for creating fixtures
|
||||||
$i = 0;
|
$i = 0;
|
||||||
$relations = explode('.', $eagerLoad);
|
$relations = explode('.', $eagerLoad);
|
||||||
|
try {
|
||||||
foreach (EagerLoadObject::get()->eagerLoad($eagerLoad) as $parentRecord) {
|
foreach (EagerLoadObject::get()->eagerLoad($eagerLoad) as $parentRecord) {
|
||||||
|
if ($i === 0) {
|
||||||
|
$this->startCountingSelectQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test first level items are handled correctly
|
||||||
$relation = $relations[0];
|
$relation = $relations[0];
|
||||||
$list = $parentRecord->$relation();
|
$listOrRecord = $parentRecord->$relation();
|
||||||
|
if (str_starts_with($iden, 'hasone') || str_starts_with($iden, 'belongsto')) {
|
||||||
|
$class = str_starts_with($iden, 'hasone') ? HasOneEagerLoadObject::class : BelongsToEagerLoadObject::class;
|
||||||
|
if ($i > 1) {
|
||||||
|
$this->assertSame(0, $listOrRecord->ID);
|
||||||
|
}
|
||||||
|
$this->assertInstanceOf($class, $listOrRecord);
|
||||||
|
} else {
|
||||||
// For any record after the first one, there should be nothing in the related list.
|
// For any record after the first one, there should be nothing in the related list.
|
||||||
$this->assertCount($i > 0 ? 0 : 2, $list);
|
// All lists, even empty ones, should be an instance of EagerLoadedList
|
||||||
|
$this->assertCount($i > 0 ? 0 : 2, $listOrRecord);
|
||||||
|
$this->assertInstanceOf(EagerLoadedList::class, $listOrRecord);
|
||||||
|
}
|
||||||
$i++;
|
$i++;
|
||||||
|
|
||||||
|
// Test second level items are handled correctly
|
||||||
if (count($relations) > 1) {
|
if (count($relations) > 1) {
|
||||||
$j = 0;
|
$j = 0;
|
||||||
foreach ($list as $relatedRecord) {
|
|
||||||
$relation = $relations[1];
|
$relation = $relations[1];
|
||||||
$list2 = $relatedRecord->$relation();
|
if (str_starts_with($iden, 'hasone') || str_starts_with($iden, 'belongsto')) {
|
||||||
|
$record2 = $listOrRecord->$relation();
|
||||||
|
$class = str_starts_with($iden, 'hasone') ? HasOneSubEagerLoadObject::class : BelongsToSubEagerLoadObject::class;
|
||||||
|
if ($j > 0) {
|
||||||
|
$this->assertSame(0, $record2->ID);
|
||||||
|
}
|
||||||
|
$this->assertInstanceOf($class, $record2);
|
||||||
|
} else {
|
||||||
// For any record after the first one, there should be nothing in the related list.
|
// For any record after the first one, there should be nothing in the related list.
|
||||||
|
// All lists, even empty ones, should be an instance of EagerLoadedList
|
||||||
|
foreach ($listOrRecord as $relatedRecord) {
|
||||||
|
$list2 = $relatedRecord->$relation();
|
||||||
$this->assertCount($j > 0 ? 0 : 2, $list2);
|
$this->assertCount($j > 0 ? 0 : 2, $list2);
|
||||||
|
$this->assertInstanceOf(EagerLoadedList::class, $list2);
|
||||||
$j++;
|
$j++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// No queries should have been run after initiating the loop
|
||||||
|
$this->assertSame(0, $this->stopCountingSelectQueries());
|
||||||
|
} finally {
|
||||||
|
$this->resetShowQueries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function provideEagerLoadingEmptyRelationLists(): array
|
public function provideEagerLoadingEmptyRelations(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
[
|
||||||
|
'hasone-onelevel',
|
||||||
|
'HasOneEagerLoadObject',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'hasone-twolevels',
|
||||||
|
'HasOneEagerLoadObject.HasOneSubEagerLoadObject',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'belongsto-onelevel',
|
||||||
|
'BelongsToEagerLoadObject',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'belongsto-twolevels',
|
||||||
|
'BelongsToEagerLoadObject.BelongsToSubEagerLoadObject',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'hasmany-onelevel',
|
'hasmany-onelevel',
|
||||||
'HasManyEagerLoadObjects',
|
'HasManyEagerLoadObjects',
|
||||||
|
Loading…
Reference in New Issue
Block a user