mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #10855 from creative-commoners/pulls/5/fix-eagerloading-performance
FIX Resolve problems with eagerloading performance
This commit is contained in:
commit
730a03d3f6
@ -1072,13 +1072,10 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
if (empty($this->eagerLoadRelations)) {
|
||||
return;
|
||||
}
|
||||
$ids = $query->column('ID');
|
||||
if (empty($ids)) {
|
||||
$topLevelIDs = $query->column('ID');
|
||||
if (empty($topLevelIDs)) {
|
||||
return;
|
||||
}
|
||||
$topLevelIDs = $ids;
|
||||
// Using ->toArray() and then iterating instead of just iterating DataList because
|
||||
// in some instances it prevents some extra SQL queries
|
||||
$prevRelationArray = [];
|
||||
foreach ($this->eagerLoadRelations as $eagerLoadRelation) {
|
||||
list(
|
||||
@ -1089,127 +1086,206 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
$hasManyIDField,
|
||||
$manyManyLastComponent
|
||||
) = $this->getEagerLoadVariables($eagerLoadRelation);
|
||||
$dataClass = $dataClasses[count($dataClasses) - 2];
|
||||
$relation = $relations[count($relations) - 1];
|
||||
$parentDataClass = $dataClasses[count($dataClasses) - 2];
|
||||
$relationName = $relations[count($relations) - 1];
|
||||
$relationDataClass = $dataClasses[count($dataClasses) - 1];
|
||||
if ($dataClass === $this->dataClass) {
|
||||
if ($parentDataClass === $this->dataClass()) {
|
||||
// When we're at "the top of a tree of nested relationships", we can just use the IDs from the query
|
||||
// This is important to do when handling multiple eager-loaded relatioship trees.
|
||||
$ids = $topLevelIDs;
|
||||
// This is important to do when handling multiple eager-loaded relationship trees.
|
||||
$parentIDs = $topLevelIDs;
|
||||
}
|
||||
// has_one
|
||||
if ($hasOneIDField) {
|
||||
$itemArray = [];
|
||||
$relationItemIDs = [];
|
||||
if ($dataClass === $dataClasses[0]) {
|
||||
while ($row = $query->record()) {
|
||||
$itemArray[] = [
|
||||
'ID' => $row['ID'],
|
||||
$hasOneIDField => $row[$hasOneIDField]
|
||||
];
|
||||
$relationItemIDs[] = $row[$hasOneIDField];
|
||||
}
|
||||
} else {
|
||||
foreach ($prevRelationArray as $itemData) {
|
||||
$itemArray[] = [
|
||||
'ID' => $itemData->ID,
|
||||
$hasOneIDField => $itemData->$hasOneIDField
|
||||
];
|
||||
$relationItemIDs[] = $itemData->$hasOneIDField;
|
||||
}
|
||||
}
|
||||
$relationArray = DataObject::get($relationDataClass)->filter(['ID' => $relationItemIDs])->toArray();
|
||||
foreach ($itemArray as $itemData) {
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$eagerLoadID = $itemData['ID'];
|
||||
if ($relationItem->ID === $itemData[$hasOneIDField]) {
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation] = $relationItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
$prevRelationArray = $relationArray;
|
||||
$ids = $relationItemIDs;
|
||||
list($prevRelationArray, $parentIDs) = $this->fetchEagerLoadHasOne(
|
||||
$query,
|
||||
$prevRelationArray,
|
||||
$hasOneIDField,
|
||||
$relationDataClass,
|
||||
$eagerLoadRelation,
|
||||
$relationName,
|
||||
$parentDataClass
|
||||
);
|
||||
// belongs_to
|
||||
} elseif ($belongsToIDField) {
|
||||
$relationArray = DataObject::get($relationDataClass)->filter([$belongsToIDField => $ids])->toArray();
|
||||
$relationItemIDs = [];
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$relationItemIDs[] = $relationItem->ID;
|
||||
$eagerLoadID = $relationItem->$belongsToIDField;
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation] = $relationItem;
|
||||
}
|
||||
$prevRelationArray = $relationArray;
|
||||
$ids = $relationItemIDs;
|
||||
list($prevRelationArray, $parentIDs) = $this->fetchEagerLoadBelongsTo(
|
||||
$parentIDs,
|
||||
$belongsToIDField,
|
||||
$relationDataClass,
|
||||
$eagerLoadRelation,
|
||||
$relationName
|
||||
);
|
||||
// has_many
|
||||
} elseif ($hasManyIDField) {
|
||||
$relationArray = DataObject::get($relationDataClass)->filter([$hasManyIDField => $ids])->toArray();
|
||||
$relationItemIDs = [];
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$relationItemIDs[] = $relationItem->ID;
|
||||
$eagerLoadID = $relationItem->$hasManyIDField;
|
||||
if (!isset($this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation])) {
|
||||
$arrayList = ArrayList::create();
|
||||
$arrayList->setDataClass($relationItem->dataClass);
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation] = $arrayList;
|
||||
}
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation]->push($relationItem);
|
||||
}
|
||||
$prevRelationArray = $relationArray;
|
||||
$ids = $relationItemIDs;
|
||||
list($prevRelationArray, $parentIDs) = $this->fetchEagerLoadHasMany(
|
||||
$parentIDs,
|
||||
$hasManyIDField,
|
||||
$relationDataClass,
|
||||
$eagerLoadRelation,
|
||||
$relationName
|
||||
);
|
||||
// many_many + belongs_many_many & many_many_through
|
||||
} elseif ($manyManyLastComponent) {
|
||||
$parentField = $manyManyLastComponent['parentField'];
|
||||
$childField = $manyManyLastComponent['childField'];
|
||||
// $join will either be:
|
||||
// - the join table name for many-many
|
||||
// - the join data class for many-many-through
|
||||
$join = $manyManyLastComponent['join'];
|
||||
// many_many_through
|
||||
if (is_a($manyManyLastComponent['relationClass'], ManyManyThroughList::class, true)) {
|
||||
$joinThroughObjs = $join::get()->filter([$parentField => $ids]);
|
||||
$relationItemIDs = [];
|
||||
$rows = [];
|
||||
foreach ($joinThroughObjs as $joinThroughObj) {
|
||||
$rows[] = [
|
||||
$parentField => $joinThroughObj->$parentField,
|
||||
$childField => $joinThroughObj->$childField
|
||||
];
|
||||
$relationItemIDs[] = $joinThroughObj->$childField;
|
||||
}
|
||||
// many_many + belongs_many_many
|
||||
} else {
|
||||
$joinTableQuery = DB::query('SELECT * FROM "' . $join . '" WHERE "' . $parentField . '" IN (' . implode(',', $ids) . ')');
|
||||
$relationItemIDs = [];
|
||||
$rows = [];
|
||||
while ($row = $joinTableQuery->record()) {
|
||||
$rows[] = [
|
||||
$parentField => $row[$parentField],
|
||||
$childField => $row[$childField]
|
||||
];
|
||||
$relationItemIDs[] = $row[$childField];
|
||||
}
|
||||
}
|
||||
$relationArray = DataObject::get($relationDataClass)->filter(['ID' => $relationItemIDs])->toArray();
|
||||
foreach ($rows as $row) {
|
||||
$eagerLoadID = $row[$parentField];
|
||||
if (!isset($this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation])) {
|
||||
$arrayList = ArrayList::create();
|
||||
$arrayList->setDataClass($manyManyLastComponent['childClass']);
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation] = $arrayList;
|
||||
}
|
||||
$relationItem = array_values(array_filter($relationArray, function ($relationItem) use ($row, $childField) {
|
||||
return $relationItem->ID === $row[$childField];
|
||||
}))[0];
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relation]->push($relationItem);
|
||||
}
|
||||
$prevRelationArray = $relationArray;
|
||||
$ids = $relationItemIDs;
|
||||
list($prevRelationArray, $parentIDs) = $this->fetchEagerLoadManyMany(
|
||||
$manyManyLastComponent,
|
||||
$parentIDs,
|
||||
$relationDataClass,
|
||||
$eagerLoadRelation,
|
||||
$relationName
|
||||
);
|
||||
} else {
|
||||
throw new LogicException('Something went wrong with the eager loading');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchEagerLoadHasOne(
|
||||
Query $query,
|
||||
array $parentRecords,
|
||||
string $hasOneIDField,
|
||||
string $relationDataClass,
|
||||
string $eagerLoadRelation,
|
||||
string $relationName,
|
||||
string $parentDataClass
|
||||
): array {
|
||||
$itemArray = [];
|
||||
$relationItemIDs = [];
|
||||
|
||||
// It's a has_one directly on the records in THIS list
|
||||
if ($parentDataClass === $this->dataClass()) {
|
||||
foreach ($query as $itemData) {
|
||||
$itemArray[] = [
|
||||
'ID' => $itemData['ID'],
|
||||
$hasOneIDField => $itemData[$hasOneIDField]
|
||||
];
|
||||
$relationItemIDs[] = $itemData[$hasOneIDField];
|
||||
}
|
||||
// It's a has_one on a list we've already eager-loaded
|
||||
} else {
|
||||
foreach ($parentRecords as $itemData) {
|
||||
$itemArray[] = [
|
||||
'ID' => $itemData->ID,
|
||||
$hasOneIDField => $itemData->$hasOneIDField
|
||||
];
|
||||
$relationItemIDs[] = $itemData->$hasOneIDField;
|
||||
}
|
||||
}
|
||||
$relationArray = DataObject::get($relationDataClass)->byIDs($relationItemIDs)->toArray();
|
||||
foreach ($itemArray as $itemData) {
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$eagerLoadID = $itemData['ID'];
|
||||
if ($relationItem->ID === $itemData[$hasOneIDField]) {
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relationName] = $relationItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$relationArray, $relationItemIDs];
|
||||
}
|
||||
|
||||
private function fetchEagerLoadBelongsTo(
|
||||
array $parentIDs,
|
||||
string $belongsToIDField,
|
||||
string $relationDataClass,
|
||||
string $eagerLoadRelation,
|
||||
string $relationName
|
||||
): array {
|
||||
// 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
|
||||
$relationArray = DataObject::get($relationDataClass)->filter([$belongsToIDField => $parentIDs])->toArray();
|
||||
$relationItemIDs = [];
|
||||
|
||||
// Store the children against the correct parent
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$relationItemIDs[] = $relationItem->ID;
|
||||
$eagerLoadID = $relationItem->$belongsToIDField;
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relationName] = $relationItem;
|
||||
}
|
||||
|
||||
return [$relationArray, $relationItemIDs];
|
||||
}
|
||||
|
||||
private function fetchEagerLoadHasMany(
|
||||
array $parentIDs,
|
||||
string $hasManyIDField,
|
||||
string $relationDataClass,
|
||||
string $eagerLoadRelation,
|
||||
string $relationName
|
||||
): array {
|
||||
// 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
|
||||
$relationArray = DataObject::get($relationDataClass)->filter([$hasManyIDField => $parentIDs])->toArray();
|
||||
$relationItemIDs = [];
|
||||
|
||||
// Store the children in an ArrayList against the correct parent
|
||||
foreach ($relationArray as $relationItem) {
|
||||
$relationItemIDs[] = $relationItem->ID;
|
||||
$eagerLoadID = $relationItem->$hasManyIDField;
|
||||
if (!isset($this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relationName])) {
|
||||
$arrayList = ArrayList::create();
|
||||
$arrayList->setDataClass($relationItem->dataClass);
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relationName] = $arrayList;
|
||||
}
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$eagerLoadID][$relationName]->push($relationItem);
|
||||
}
|
||||
|
||||
return [$relationArray, $relationItemIDs];
|
||||
}
|
||||
|
||||
private function fetchEagerLoadManyMany(
|
||||
array $manyManyLastComponent,
|
||||
array $parentIDs,
|
||||
string $relationDataClass,
|
||||
string $eagerLoadRelation,
|
||||
string $relationName
|
||||
): array {
|
||||
$parentIDField = $manyManyLastComponent['parentField'];
|
||||
$childIDField = $manyManyLastComponent['childField'];
|
||||
// $join will either be:
|
||||
// - the join table name for many-many
|
||||
// - the join data class for many-many-through
|
||||
$join = $manyManyLastComponent['join'];
|
||||
|
||||
// many_many_through
|
||||
if (is_a($manyManyLastComponent['relationClass'], ManyManyThroughList::class, true)) {
|
||||
$joinThroughObjs = DataObject::get($join)->filter([$parentIDField => $parentIDs]);
|
||||
$relationItemIDs = [];
|
||||
$joinRows = [];
|
||||
foreach ($joinThroughObjs as $joinThroughObj) {
|
||||
$joinRows[] = [
|
||||
$parentIDField => $joinThroughObj->$parentIDField,
|
||||
$childIDField => $joinThroughObj->$childIDField
|
||||
];
|
||||
$relationItemIDs[] = $joinThroughObj->$childIDField;
|
||||
}
|
||||
// many_many + belongs_many_many
|
||||
} else {
|
||||
$joinTableQuery = DB::query('SELECT * FROM "' . $join . '" WHERE "' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')');
|
||||
$relationItemIDs = $joinTableQuery->column($childIDField);
|
||||
$joinRows = $joinTableQuery;
|
||||
}
|
||||
|
||||
// Get ALL of the items for this relation up front, for ALL of the parents
|
||||
// Fetched as a map so we can get the ID for all records up front (instead of in another nested loop)
|
||||
// Fetched after that as an array because for some reason that performs better in the loop
|
||||
// Note that "Me" is a method on ViewableData that returns $this - i.e. that is the actual DataObject record
|
||||
$relationArray = DataObject::get($relationDataClass)->byIDs($relationItemIDs)->map('ID', 'Me')->toArray();
|
||||
|
||||
// Store the children in an ArrayList against the correct parent
|
||||
foreach ($joinRows as $row) {
|
||||
$parentID = $row[$parentIDField];
|
||||
$childID = $row[$childIDField];
|
||||
$relationItem = $relationArray[$childID];
|
||||
|
||||
if (!isset($this->eagerLoadedData[$eagerLoadRelation][$parentID][$relationName])) {
|
||||
$arrayList = ArrayList::create();
|
||||
$arrayList->setDataClass($relationItem->dataClass);
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$parentID][$relationName] = $arrayList;
|
||||
}
|
||||
$this->eagerLoadedData[$eagerLoadRelation][$parentID][$relationName]->push($relationItem);
|
||||
}
|
||||
|
||||
return [$relationArray, $relationItemIDs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Eager load relations for DataObjects in this DataList including nested relations
|
||||
*
|
||||
@ -1235,15 +1311,16 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
$message = "Eager loading only supports up to 3 levels of nesting, passed $count levels - $relation";
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if ($i === 0) {
|
||||
$arr[] = $parts[$i];
|
||||
} else {
|
||||
$arr[] = $arr[count($arr) - 1] . '.' . $parts[$i];
|
||||
}
|
||||
// Add each relation in the chain as its own entry to be eagerloaded
|
||||
// e.g. for "Players.Teams.Coaches" you'll have three entries:
|
||||
// "Players", "Players.Teams", and "Players.Teams.Coaches
|
||||
$usedParts = [];
|
||||
foreach ($parts as $part) {
|
||||
$usedParts[] = $part;
|
||||
$arr[] = implode('.', $usedParts);
|
||||
}
|
||||
}
|
||||
$this->eagerLoadRelations = array_merge($this->eagerLoadRelations, $arr);
|
||||
$this->eagerLoadRelations = array_unique(array_merge($this->eagerLoadRelations, $arr));
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user