Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEXT-11081 - Improve performance of RepositoryIterator and remove a possible next request #3602

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,11 @@
---
title: Prevent predictable empty request in RepositoryIterator and improve developer experience
issue: NEXT-11081
author: Joshua Behrens
author_email: code@joshua-behrens.de
author_github: @JoshuaBehrens
---
# Core
* Added `\Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator::reset` to either remove `increment` filter if the iterated entity has an auto increment column or otherwise removes the offset. This helps to implement iterator resets without knowing how the iteration is implemented
* Added internal state into `\Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator`, whether the previous `fetch` or `fetchIds` result had less items than expected and therefore the next search will be empty anyways
* Added methods `iterateIds` and `iterateEntities` to class `\Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator` to automatically perform iteration on respectively `fetchIds` and `fetch`
3 changes: 0 additions & 3 deletions src/Core/Checkout/Cart/RuleLoader.php
Expand Up @@ -50,9 +50,6 @@ public function load(Context $context): RuleCollection
$rules->add($rule);
}
}
if ($result->count() < 500) {
break;
}
}

return $rules;
Expand Down
Expand Up @@ -3,8 +3,10 @@
namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\PartialEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
Expand All @@ -28,6 +30,8 @@ class RepositoryIterator

private bool $autoIncrement = false;

private bool $endReached = false;

/**
* @param EntityRepository<TEntityCollection> $repository
*/
Expand Down Expand Up @@ -56,6 +60,23 @@ public function __construct(
$this->context = clone $context;
}

public function reset(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe name it rewind ? https://www.php.net/manual/en/function.rewind.php That's what php use for such things.

A simple php doc would also be nice:

Rewinds the Iterator to the beginning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I start calling it rewind, others would expect other iterator methods, right? I mean I can rename it but I assume, that this is creating confusion

{
if ($this->autoIncrement) {
$filters = $this->criteria->getFilters();
$this->criteria->resetFilters();
unset($filters['increment']);

foreach ($filters as $filterKey => $filter) {
$this->criteria->setFilter($filterKey, $filter);
}
} else {
$this->criteria->setOffset(0);
}

$this->endReached = false;
}

public function getTotal(): int
{
$criteria = clone $this->criteria;
Expand All @@ -71,19 +92,29 @@ public function getTotal(): int
*/
public function fetchIds(): ?array
{
if ($this->endReached) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

$repositoryIterator->loop(function(array $entities) {
            
});

$repositoryIterator->loopIds(function(array $ids) {
            
});

In this case we don't have to leak all of this internal "rewind" etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought. How would one break this loop? Return false? In my suggestions of the getIterator implementation you have the option to just not continue using the iterator. But I like the closure approach. Feels not very php though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add some generators as we once written about. This allows for easier flow control by using language features.

return null;
}

$this->criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);

$ids = $this->repository->searchIds($this->criteria, $this->context);

$values = $ids->getIds();

if (empty($values)) {
$this->endReached = true;

return null;
}

if (!$this->autoIncrement) {
$this->criteria->setOffset($this->criteria->getOffset() + $this->criteria->getLimit());

if (count($values) < $this->criteria->getLimit()) {
$this->endReached = true;
}

return $values;
}

Expand All @@ -95,6 +126,10 @@ public function fetchIds(): ?array
$increment = $ids->getDataFieldOfId($last, 'autoIncrement') ?? 0;
$this->criteria->setFilter('increment', new RangeFilter('autoIncrement', [RangeFilter::GT => $increment]));

if (count($values) < $this->criteria->getLimit()) {
$this->endReached = true;
}

return $values;
}

Expand All @@ -103,6 +138,10 @@ public function fetchIds(): ?array
*/
public function fetch(): ?EntitySearchResult
{
if ($this->endReached) {
return null;
}

$this->criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);

$result = $this->repository->search(clone $this->criteria, $this->context);
Expand All @@ -111,9 +150,47 @@ public function fetch(): ?EntitySearchResult
$this->criteria->setOffset($this->criteria->getOffset() + $this->criteria->getLimit());

if (empty($result->getIds())) {
$this->endReached = true;

return null;
}

if ($result->count() < $this->criteria->getLimit()) {
$this->endReached = true;
}

return $result;
}

/**
* @return iterable<Entity|PartialEntity>
*/
public function iterateEntities(): iterable
{
try {
while (($entityResult = $this->fetch()) instanceof EntitySearchResult) {
// yield from is okay, as getElements keys by unique key
yield from $entityResult->getElements();
}
} finally {
$this->reset();
}
}

/**
* @return iterable<string>|iterable<array<string, string>>
*/
public function iterateIds(): iterable
{
try {
while (\is_array($ids = $this->fetchIds())) {
// do not use yield from to ensure re-keying
foreach ($ids as $id) {
yield $id;
}
}
} finally {
$this->reset();
}
}
}