ENH Add polymorphic has_one support to eager loading #11203

This commit is contained in:
Dominik Beerbohm 2024-04-19 13:18:41 +02:00
parent cfeb678816
commit 7e49952a02
No known key found for this signature in database
GPG Key ID: 0AD022828C113744
4 changed files with 160 additions and 42 deletions

View File

@ -979,7 +979,10 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
return [
$hasOneComponent,
'has_one',
$relationName . 'ID',
[
'joinField' => $relationName . 'ID',
'joinClass' => $relationName . 'Class',
],
];
}
$belongsToComponent = $schema->belongsToComponent($parentDataClass, $relationName);
@ -1109,7 +1112,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
private function fetchEagerLoadHasOne(
Query|array $parents,
string $hasOneIDField,
array $hasOneRelation,
string $relationDataClass,
string $relationChain,
string $relationName,
@ -1120,6 +1123,9 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
throw new LogicException("Cannot manipulate eagerloading query for $relationType relation $relationName");
}
$hasOneIDField = $hasOneRelation['joinField'];
$hasOneClassField = $hasOneRelation['joinClass'];
$fetchedIDs = [];
$addTo = [];
@ -1128,41 +1134,62 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
if (is_array($parentData)) {
// $parentData represents a record in this DataList
$hasOneID = $parentData[$hasOneIDField];
$fetchedIDs[] = $hasOneID;
$addTo[$hasOneID][] = $parentData['ID'];
if ($hasOneID > 0) {
// Class field is only set for polymorphic has_one relations
$hasOneClass = $parentData[$hasOneClassField] ?? $relationDataClass;
$fetchedIDs[$hasOneClass][$hasOneID] = $hasOneID;
$addTo[$hasOneClass][$hasOneID][] = $parentData['ID'];
}
} elseif ($parentData instanceof DataObject) {
// $parentData represents another has_one record
$hasOneID = $parentData->$hasOneIDField;
$fetchedIDs[] = $hasOneID;
$addTo[$hasOneID][] = $parentData;
if ($hasOneID > 0) {
// Class field is only set for polymorphic has_one relations
$hasOneClass = $parentData->$hasOneClassField ?? $relationDataClass;
$fetchedIDs[$hasOneClass][$hasOneID] = $hasOneID;
$addTo[$hasOneClass][$hasOneID][] = $parentData;
}
} elseif ($parentData instanceof EagerLoadedList) {
// $parentData represents a has_many or many_many relation
foreach ($parentData->getRows() as $parentRow) {
$hasOneID = $parentRow[$hasOneIDField];
$fetchedIDs[] = $hasOneID;
$addTo[$hasOneID][] = ['ID' => $parentRow['ID'], 'list' => $parentData];
if ($hasOneID > 0) {
// Class field is only set for polymorphic has_one relations
$hasOneClass = $parentRow[$hasOneClassField] ?? $relationDataClass;
$fetchedIDs[$hasOneClass][$hasOneID] = $hasOneID;
$addTo[$hasOneClass][$hasOneID][] = ['ID' => $parentRow['ID'], 'list' => $parentData];
}
}
} else {
throw new LogicException("Invalid parent for eager loading $relationType relation $relationName");
}
}
$fetchedRecords = DataObject::get($relationDataClass)->byIDs($fetchedIDs)->toArray();
$fetchedRecords = [];
// Add each fetched record to the appropriate place
foreach ($fetchedRecords as $fetched) {
if (isset($addTo[$fetched->ID])) {
foreach ($addTo[$fetched->ID] as $addHere) {
if ($addHere instanceof DataObject) {
$addHere->setEagerLoadedData($relationName, $fetched);
} elseif (is_array($addHere)) {
$addHere['list']->addEagerLoadedData($relationName, $addHere['ID'], $fetched);
} else {
$this->eagerLoadedData[$relationChain][$addHere][$relationName] = $fetched;
foreach ($fetchedIDs as $class => $ids) {
foreach (DataObject::get($class)->byIDs($ids) as $fetched) {
$fetchedRecords[] = $fetched;
if (isset($addTo[$class][$fetched->ID])) {
foreach ($addTo[$class][$fetched->ID] as $addHere) {
if ($addHere instanceof DataObject) {
$addHere->setEagerLoadedData($relationName, $fetched);
} elseif (is_array($addHere)) {
$addHere['list']->addEagerLoadedData($relationName, $addHere['ID'], $fetched);
} else {
$this->eagerLoadedData[$relationChain][$addHere][$relationName] = $fetched;
}
}
} else {
throw new LogicException("Couldn't find parent for record $fetchedID on $relationType relation $relationName");
}
} else {
throw new LogicException("Couldn't find parent for record $fetchedID on $relationType relation $relationName");
}
}

View File

@ -13,6 +13,7 @@ use SilverStripe\ORM\DB;
use SilverStripe\ORM\EagerLoadedList;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Tests\DataListTest\EagerLoading\EagerLoadObject;
use SilverStripe\ORM\Tests\DataListTest\EagerLoading\EagerLoadSubClassObject;
use SilverStripe\ORM\Tests\DataListTest\EagerLoading\HasOneEagerLoadObject;
use SilverStripe\ORM\Tests\DataListTest\EagerLoading\HasOneSubEagerLoadObject;
use SilverStripe\ORM\Tests\DataListTest\EagerLoading\HasOneSubSubEagerLoadObject;
@ -1591,28 +1592,12 @@ class DataListEagerLoadingTest extends SapphireTest
public function testHasOneMultipleAppearance(): void
{
$this->provideHasOneObjects();
$this->validateMultipleAppearance(6, EagerLoadObject::get());
$this->validateMultipleAppearance(2, EagerLoadObject::get()->eagerLoad('HasOneEagerLoadObject'));
$items = $this->provideHasOneObjects();
$this->validateMultipleAppearance($items, 6, EagerLoadObject::get());
$this->validateMultipleAppearance($items, 2, EagerLoadObject::get()->eagerLoad('HasOneEagerLoadObject'));
}
protected function validateMultipleAppearance(int $expected, DataList $list): void
{
try {
$this->startCountingSelectQueries();
/** @var EagerLoadObject $item */
foreach ($list as $item) {
$item->HasOneEagerLoadObject()->Title;
}
$this->assertSame($expected, $this->stopCountingSelectQueries());
} finally {
$this->resetShowQueries();
}
}
protected function provideHasOneObjects(): void
protected function provideHasOneObjects(): array
{
$subA = new HasOneEagerLoadObject();
$subA->Title = 'A';
@ -1655,5 +1640,102 @@ class DataListEagerLoadingTest extends SapphireTest
$baseF->Title = 'F';
$baseF->HasOneEagerLoadObjectID = 0;
$baseF->write();
return [
$baseA->ID => [$subA->ClassName, $subA->ID],
$baseB->ID => [$subA->ClassName, $subA->ID],
$baseC->ID => [$subB->ClassName, $subB->ID],
$baseD->ID => [$subC->ClassName, $subC->ID],
$baseE->ID => [$subB->ClassName, $subB->ID],
$baseF->ID => [null, 0],
];
}
public function testPolymorphEagerLoading(): void
{
$items = $this->providePolymorphHasOne();
$this->validateMultipleAppearance($items, 5, EagerLoadObject::get(), 'HasOnePolymorphObject');
$this->validateMultipleAppearance($items, 4, EagerLoadObject::get()->eagerLoad('HasOnePolymorphObject'), 'HasOnePolymorphObject');
}
protected function providePolymorphHasOne(): array
{
$subA = new HasOneEagerLoadObject();
$subA->Title = 'A';
$subA->write();
$subB = new HasOneEagerLoadObject();
$subB->Title = 'B';
$subB->write();
$subC = new HasOneSubSubEagerLoadObject();
$subC->Title = 'C';
$subC->write();
$subD = new EagerLoadSubClassObject();
$subD->Title = 'D';
$subD->write();
$baseA = new EagerLoadObject();
$baseA->Title = 'A';
$baseA->HasOnePolymorphObjectClass = $subA->ClassName;
$baseA->HasOnePolymorphObjectID = $subA->ID;
$baseA->write();
$baseB = new EagerLoadObject();
$baseB->Title = 'B';
$baseB->HasOnePolymorphObjectClass = $subB->ClassName;
$baseB->HasOnePolymorphObjectID = $subB->ID;
$baseB->write();
$baseC = new EagerLoadObject();
$baseC->Title = 'C';
$baseC->HasOnePolymorphObjectClass = $subC->ClassName;
$baseC->HasOnePolymorphObjectID = $subC->ID;
$baseC->write();
$baseD = new EagerLoadObject();
$baseD->Title = 'D';
$baseD->HasOnePolymorphObjectClass = $subD->ClassName;
$baseD->HasOnePolymorphObjectID = $subD->ID;
$baseD->write();
$baseE = new EagerLoadObject();
$baseE->Title = 'E';
$baseE->HasOnePolymorphObjectClass = null;
$baseE->HasOnePolymorphObjectID = 0;
$baseE->write();
return [
$baseA->ID => [$subA->ClassName, $subA->ID],
$baseB->ID => [$subB->ClassName, $subB->ID],
$baseC->ID => [$subC->ClassName, $subC->ID],
$baseD->ID => [$subD->ClassName, $subD->ID],
$baseE->ID => [null, 0],
];
}
protected function validateMultipleAppearance(
array $expectedRelations,
int $expected,
DataList $list,
string $relation = 'HasOneEagerLoadObject',
): void {
try {
$this->startCountingSelectQueries();
/** @var EagerLoadObject $item */
foreach ($list as $item) {
$rel = $item->{$relation}();
$this->assertArrayHasKey($item->ID, $expectedRelations, $relation . ' should be loaded');
$this->assertEquals($expectedRelations[$item->ID][0], $rel?->ID ? $rel?->ClassName : null);
$this->assertEquals($expectedRelations[$item->ID][1], $rel?->ID ?? 0);
}
$this->assertSame($expected, $this->stopCountingSelectQueries());
} finally {
$this->resetShowQueries();
}
}
}

View File

@ -14,7 +14,8 @@ class EagerLoadObject extends DataObject implements TestOnly
];
private static $has_one = [
'HasOneEagerLoadObject' => HasOneEagerLoadObject::class
'HasOneEagerLoadObject' => HasOneEagerLoadObject::class,
'HasOnePolymorphObject' => DataObject::class,
];
private static $belongs_to = [

View File

@ -0,0 +1,8 @@
<?php
namespace SilverStripe\ORM\Tests\DataListTest\EagerLoading;
class EagerLoadSubClassObject extends HasOneSubSubEagerLoadObject
{
}