Skip to content

Commit

Permalink
ENH Add polymorphic has_one support to eager loading
Browse files Browse the repository at this point in the history
  • Loading branch information
beerbohmdo committed Apr 19, 2024
1 parent cfeb678 commit 880b213
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 120 deletions.
145 changes: 47 additions & 98 deletions src/ORM/DataList.php
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,10 @@ private function getEagerLoadVariables(string $relationChain, string $relationNa
return [
$hasOneComponent,
'has_one',
$relationName . 'ID',
[
'joinField' => $relationName . 'ID',
'joinClass' => $relationName . 'Class',
],
];
}
$belongsToComponent = $schema->belongsToComponent($parentDataClass, $relationName);
Expand Down Expand Up @@ -1030,86 +1033,9 @@ private function executeQuery(): Query
return $query;
}

private function fetchEagerLoadRelations(Query $query): void
{
if (empty($this->eagerLoadRelationChains)) {
return;
}
$topLevelIDs = $query->column('ID');
if (empty($topLevelIDs)) {
return;
}

foreach ($this->eagerLoadRelationChains as $relationChain) {
$parentDataClass = $this->dataClass();
$parentIDs = $topLevelIDs;
$parentRelationData = $query;
$chainToDate = [];
foreach (explode('.', $relationChain) as $relationName) {
/** @var Query|array<DataObject|EagerLoadedList> $parentRelationData */
$chainToDate[] = $relationName;
list(
$relationDataClass,
$relationType,
$relationComponent,
) = $this->getEagerLoadVariables($relationChain, $relationName, $parentDataClass);

switch ($relationType) {
case 'has_one':
list($parentRelationData, $parentIDs) = $this->fetchEagerLoadHasOne(
$parentRelationData,
$relationComponent,
$relationDataClass,
implode('.', $chainToDate),
$relationName,
$relationType
);
break;
case 'belongs_to':
list($parentRelationData, $parentIDs) = $this->fetchEagerLoadBelongsTo(
$parentRelationData,
$parentIDs,
$relationComponent,
$relationDataClass,
implode('.', $chainToDate),
$relationName,
$relationType
);
break;
case 'has_many':
list($parentRelationData, $parentIDs) = $this->fetchEagerLoadHasMany(
$parentRelationData,
$parentIDs,
$relationComponent,
$relationDataClass,
implode('.', $chainToDate),
$relationName,
$relationType
);
break;
case 'many_many':
list($parentRelationData, $parentIDs) = $this->fetchEagerLoadManyMany(
$parentRelationData,
$relationComponent,
$parentIDs,
$relationDataClass,
implode('.', $chainToDate),
$relationName,
$parentDataClass,
$relationType
);
break;
default:
throw new LogicException("Unexpected relation type $relationType");
}
$parentDataClass = $relationDataClass;
}
}
}

private function fetchEagerLoadHasOne(
Query|array $parents,
string $hasOneIDField,
array $hasOneRelation,
string $relationDataClass,
string $relationChain,
string $relationName,
Expand All @@ -1120,6 +1046,11 @@ private function fetchEagerLoadHasOne(
throw new LogicException("Cannot manipulate eagerloading query for $relationType relation $relationName");
}

$hasOneIDField = $hasOneRelation['joinField'];
$hasOneClassField = $hasOneRelation['joinClass'];

$schema = DataObject::getSchema();

$fetchedIDs = [];
$addTo = [];

Expand All @@ -1128,41 +1059,59 @@ private function fetchEagerLoadHasOne(
if (is_array($parentData)) {
// $parentData represents a record in this DataList
$hasOneID = $parentData[$hasOneIDField];
$fetchedIDs[] = $hasOneID;
$addTo[$hasOneID][] = $parentData['ID'];

if ($hasOneID > 0) {
$hasOneClass = $schema->baseDataClass($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) {
$hasOneClass = $schema->baseDataClass($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) {
$hasOneClass = $schema->baseDataClass($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");
}
}

Expand Down
111 changes: 90 additions & 21 deletions tests/php/ORM/DataListEagerLoadingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1588,31 +1588,14 @@ public function testManipulatingEagerloadingQuery(string $relationType, array $r
}
}
}

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';
Expand Down Expand Up @@ -1655,5 +1638,91 @@ protected function provideHasOneObjects(): void
$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, 4, EagerLoadObject::get(), 'HasOnePolymorphObject');
$this->validateMultipleAppearance($items, 3, 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();

$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 = null;
$baseD->HasOnePolymorphObjectID = 0;
$baseD->write();

return [
$baseA->ID => [$subA->ClassName, $subA->ID],
$baseB->ID => [$subB->ClassName, $subB->ID],
$baseC->ID => [$subC->ClassName, $subC->ID],
$baseD->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);
$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();
}
}
}
3 changes: 2 additions & 1 deletion tests/php/ORM/DataListTest/EagerLoading/EagerLoadObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down

0 comments on commit 880b213

Please sign in to comment.