Skip to content

Commit

Permalink
NEW Allow pre-filtering and pre-sorting eagerloaded data
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Feb 14, 2024
1 parent 5f355fb commit 77851f8
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 11 deletions.
88 changes: 78 additions & 10 deletions src/ORM/DataList.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Filters\SearchFilterable;
use Symfony\Component\Validator\Tests\Constraints\Limit;

/**
* Implements a "lazy loading" DataObjectSet.
Expand Down Expand Up @@ -1115,6 +1116,11 @@ private function fetchEagerLoadHasOne(
string $relationName,
string $relationType
): array {
// Throw exception if developers try to filter/sort a has_one relation
if ($this->eagerLoadAllRelations[$relationChain] !== null) {
throw new LogicException("Cannot filter/sort $relationType relation $relationName");
}

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

Expand Down Expand Up @@ -1182,6 +1188,11 @@ private function fetchEagerLoadBelongsTo(
string $relationName,
string $relationType
): array {
// Throw exception if developers try to filter/sort a belongs_to relation
if ($this->eagerLoadAllRelations[$relationChain] !== null) {
throw new LogicException("Cannot filter/sort $relationType relation $relationName");
}

$belongsToIDField = $component['joinField'];
// 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
Expand Down Expand Up @@ -1222,9 +1233,11 @@ private function fetchEagerLoadHasMany(
string $relationName,
string $relationType
): array {
$fetchList = DataObject::get($relationDataClass)->filter([$hasManyIDField => $parentIDs]);
$fetchList = $this->applyPreFilteringToEagerloading($fetchList, $relationChain, $relationType);
// 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
$fetchedRows = DataObject::get($relationDataClass)->filter([$hasManyIDField => $parentIDs])->getFinalisedQuery();
$fetchedRows = $fetchList->getFinalisedQuery();
$fetchedIDs = [];
$eagerLoadedLists = [];

Expand Down Expand Up @@ -1278,10 +1291,6 @@ private function fetchEagerLoadManyMany(
$fetchedIDs = [];
$eagerLoadedLists = [];

// Get the join records so we can correctly identify which children belong to which parents
// This also holds extra fields data
$joinRows = DB::query('SELECT * FROM "' . $joinTable . '" WHERE "' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')');

// Use a real RelationList here so that the extraFields and join record are correctly fetched for all relations
// There's a lot of special handling for things like DBComposite extra fields, etc.
if ($joinClass !== null) {
Expand All @@ -1308,13 +1317,19 @@ private function fetchEagerLoadManyMany(
$relationListClass = get_class($relationList);

// Get ALL of the items for this relation up front, for ALL of the parents
$fetchedRows = $relationList->forForeignID($parentIDs)->getFinalisedQuery();
$fetchList = $relationList->forForeignID($parentIDs);
$fetchList = $this->applyPreFilteringToEagerloading($fetchList, $relationChain, $relationType);
$fetchedRows = $fetchList->getFinalisedQuery();

foreach ($fetchedRows as $row) {
$fetchedRowsArray[$row['ID']] = $row;
$fetchedIDs[] = $row['ID'];
}

// Get the join records so we can correctly identify which children belong to which parents
// This also holds extra fields data
$joinRows = DB::query('SELECT * FROM "' . $joinTable . '" WHERE "' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ') AND ' . $childIDField . ' IN (' . implode(',', $fetchedIDs) . ')');

// Store the children in an EagerLoadedList against the correct parent
foreach ($joinRows as $row) {
$parentID = $row[$parentIDField];
Expand Down Expand Up @@ -1398,6 +1413,26 @@ private function addEagerLoadedDataToParent(
}
}

/**
* NOTE: Do not change `DataList` to `static` here - subclasses of DataList must still accept DataList arguments
* and return DataList!
*/
private function applyPreFilteringToEagerloading(DataList $fetchList, string $relationChain, string $relationType): DataList
{
$filterCallback = $this->eagerLoadAllRelations[$relationChain];
if ($filterCallback !== null) {
$fetchList = $filterCallback($fetchList);
}
if (!($fetchList instanceof DataList)) {
throw new LogicException("Eagerloading callback for $relationType relation $relationChain must return a DataList.");
}
$limit = $fetchList->dataQuery->getLimit();
if (!empty($limit) && ($limit['start'] !== 0 || $limit['limit'] !== null)) {
throw new LogicException("Cannot apply limit to eagerloaded data for $relationType relation $relationChain.");
}
return $fetchList;
}

private function fillEmptyEagerLoadedRelations(
Query|array $parents,
array $missingParentIDs,
Expand Down Expand Up @@ -1447,8 +1482,17 @@ private function fillEmptyEagerLoadedRelations(
* You can specify nested relations by using dot notation, and you can also pass in multiple relations.
* When specifying nested relations there is a maximum of 3 levels of relations allowed i.e. 2 dots
*
* Example:
* Examples:
* <code>
* $myDataList->eagerLoad('MyRelation.NestedRelation.EvenMoreNestedRelation', 'DifferentRelation')
* </code>
*
* <code>
* $myDataList->eagerLoad([
* 'MyRelation.NestedRelation.EvenMoreNestedRelation',
* 'DifferentRelation' => fn (DataList $list) => $list->filter($filterArgs),
* ]);
* </code>
*
* IMPORTANT: Calling eagerLoad() will cause any relations on DataObjects to be returned as an EagerLoadedList
* instead of a subclass of DataList such as HasManyList i.e. MyDataObject->MyHasManyRelation() returns an EagerLoadedList
Expand All @@ -1458,8 +1502,29 @@ private function fillEmptyEagerLoadedRelations(
public function eagerLoad(...$relationChains): static
{
$list = clone $this;
foreach ($relationChains as $relationChain) {
// Don't add any relations we've added before

// If an array is passed in directly, treat it as though $relationChains wasn't spread.
if (count($relationChains) === 1 && is_array($relationChains[array_key_first($relationChains)])) {
$relationChains = $relationChains[array_key_first($relationChains)];
}

foreach ($relationChains as $relationChain => $callback) {
// Allow non-associative arrays
if (is_numeric($relationChain)) {
$relationChain = $callback;
$callback = null;
}

// Reject non-callable in associative array
if ($callback !== null && !is_callable($callback)) {
throw new LogicException(
'Value of associative array must be a callable.'
. 'If you don\'t want to pre-filter the list, use an indexed array.'
);
}

// Don't add any relations we've added before.
// Note we explicitly cannot use `isset` here, because most of the values are set to `null`.
if (array_key_exists($relationChain, $list->eagerLoadAllRelations)) {
continue;
}
Expand All @@ -1480,8 +1545,11 @@ public function eagerLoad(...$relationChains): static
// Keep track of what we've seen before so we don't accidentally add a level 1 relation
// (e.g. "Players") to the chains list when we already have it as part of a longer chain
// (e.g. "Players.Teams")
$list->eagerLoadAllRelations[$item] = $item;
$list->eagerLoadAllRelations[$item] ??= null;
}
// Set the callback for this chain
$list->eagerLoadAllRelations[$relationChain] = $callback;
// Set the relation chain to be loaded
$list->eagerLoadRelationChains[$relationChain] = $relationChain;
}
return $list;
Expand Down
8 changes: 8 additions & 0 deletions src/ORM/DataQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,14 @@ public function limit(?int $limit, int $offset = 0): static
return $this;
}

/**
* Get the current limit of this query.
*/
public function getLimit(): array
{
return $this->query->getLimit();
}

/**
* Set whether this query should be distinct or not.
*
Expand Down
1 change: 0 additions & 1 deletion src/Security/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
*/
class Group extends DataObject
{

private static $db = [
"Title" => "Varchar(255)",
"Description" => "Text",
Expand Down

0 comments on commit 77851f8

Please sign in to comment.