perf(directory): memoize arcadia brand lookups to eliminate N+1 queries during INCO sheet hydration (F-2026-0003)
This commit is contained in:
parent
861a1a8146
commit
18b83b6e67
3 changed files with 124 additions and 9 deletions
|
|
@ -12,27 +12,43 @@ use Infra\Database\Models\ArcadiaBrand as ArcadiaBrandModel;
|
|||
|
||||
final class EloquentArcadiaBrandDirectory implements ArcadiaBrandDirectory
|
||||
{
|
||||
/** @var array<string, ?ArcadiaBrand> */
|
||||
private array $cache = [];
|
||||
|
||||
#[\Override]
|
||||
public function findAll(): array
|
||||
{
|
||||
return array_values(
|
||||
ArcadiaBrandModel::query()
|
||||
->get()
|
||||
->map(fn (ArcadiaBrandModel $model): ArcadiaBrand => $this->toDomain($model))
|
||||
->all()
|
||||
);
|
||||
$models = ArcadiaBrandModel::query()->get();
|
||||
$items = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$domain = $this->toDomain($model);
|
||||
$this->cache[$domain->id->value] = $domain;
|
||||
$items[] = $domain;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function find(ArcadiaId $id): ?ArcadiaBrand
|
||||
{
|
||||
if (array_key_exists($id->value, $this->cache)) {
|
||||
return $this->cache[$id->value];
|
||||
}
|
||||
|
||||
$model = ArcadiaBrandModel::query()->find($id->value);
|
||||
|
||||
if ($model === null) {
|
||||
$this->cache[$id->value] = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->toDomain($model);
|
||||
$domain = $this->toDomain($model);
|
||||
$this->cache[$id->value] = $domain;
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
private function toDomain(ArcadiaBrandModel $model): ArcadiaBrand
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use Infra\Adapters\Repositories\EloquentBrandRepository;
|
|||
use Infra\Cache\BrandCache;
|
||||
use Infra\Config\ArcadiaStorageConfig;
|
||||
use Infra\Usecase\LaravelExecutor;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\TransactionalDomainEventDispatcher;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
|
|
@ -38,14 +39,14 @@ class DomainServiceProvider extends ServiceProvider
|
|||
$this->app->bind(IncoSheetDirectory::class, EloquentIncoSheetDirectory::class);
|
||||
$this->app->bind(IncoShortSheetDirectory::class, EloquentIncoShortSheetDirectory::class);
|
||||
$this->app->bind(IncoCompositionDirectory::class, EloquentIncoCompositionDirectory::class);
|
||||
$this->app->bind(ArcadiaBrandDirectory::class, EloquentArcadiaBrandDirectory::class);
|
||||
$this->app->scoped(ArcadiaBrandDirectory::class, EloquentArcadiaBrandDirectory::class);
|
||||
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
|
||||
$this->app->bind(ClientAuditLogDirectory::class, EloquentClientAuditLogDirectory::class);
|
||||
|
||||
$this->app->bind(BrandCache::class, static function (Application $app): BrandCache {
|
||||
$storageConfig = $app->make(ArcadiaStorageConfig::class);
|
||||
$cache = Cache::store($storageConfig->cacheStore);
|
||||
$logger = $app->make(\Psr\Log\LoggerInterface::class);
|
||||
$logger = $app->make(LoggerInterface::class);
|
||||
|
||||
return new BrandCache($cache, $logger);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration\Infra\Adapters\Directories;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand as ArcadiaBrandDomain;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Infra\Adapters\Directories\EloquentArcadiaBrandDirectory;
|
||||
use Infra\Database\Models\ArcadiaBrand;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
covers(EloquentArcadiaBrandDirectory::class);
|
||||
|
||||
describe('EloquentArcadiaBrandDirectory Integration Tests', function (): void {
|
||||
beforeEach(function (): void {
|
||||
ArcadiaBrand::query()->delete();
|
||||
ArcadiaBrand::query()->create(['code' => 1, 'libelle' => 'Le Gaulois']);
|
||||
ArcadiaBrand::query()->create(['code' => 2, 'libelle' => 'Maître CoQ']);
|
||||
});
|
||||
|
||||
it('finds an arcadia brand by ArcadiaId and memoizes the result in memory', function (): void {
|
||||
// GIVEN
|
||||
$directory = new EloquentArcadiaBrandDirectory();
|
||||
$arcadiaId = new ArcadiaId('1');
|
||||
|
||||
// WHEN: First lookup (hits database)
|
||||
$first = $directory->find($arcadiaId);
|
||||
|
||||
// THEN
|
||||
expect($first)->toBeInstanceOf(ArcadiaBrandDomain::class)
|
||||
->and($first?->id->value)->toBe('1')
|
||||
->and($first?->name->value)->toBe('Le Gaulois');
|
||||
|
||||
// WHEN: Second lookup - DB query count should not increase because of in-memory caching
|
||||
$queryCount = 0;
|
||||
DB::listen(function () use (&$queryCount): void {
|
||||
$queryCount++;
|
||||
});
|
||||
|
||||
$second = $directory->find($arcadiaId);
|
||||
|
||||
// THEN: Second call returns the exact memoized entity with 0 extra queries
|
||||
expect($second)->toBe($first)
|
||||
->and($queryCount)->toBe(0);
|
||||
});
|
||||
|
||||
it('returns null when arcadia brand is not found and caches the null result', function (): void {
|
||||
// GIVEN
|
||||
$directory = new EloquentArcadiaBrandDirectory();
|
||||
$unknownId = new ArcadiaId('999');
|
||||
|
||||
// WHEN: First lookup for non-existent brand
|
||||
$first = $directory->find($unknownId);
|
||||
|
||||
// THEN
|
||||
expect($first)->toBeNull();
|
||||
|
||||
// WHEN: Second lookup
|
||||
$queryCount = 0;
|
||||
DB::listen(function () use (&$queryCount): void {
|
||||
$queryCount++;
|
||||
});
|
||||
|
||||
$second = $directory->find($unknownId);
|
||||
|
||||
// THEN
|
||||
expect($second)->toBeNull()
|
||||
->and($queryCount)->toBe(0);
|
||||
});
|
||||
|
||||
it('finds all arcadia brands and populates the cache', function (): void {
|
||||
// GIVEN
|
||||
$directory = new EloquentArcadiaBrandDirectory();
|
||||
|
||||
// WHEN
|
||||
$all = $directory->findAll();
|
||||
|
||||
// THEN
|
||||
expect($all)->toHaveCount(2);
|
||||
|
||||
// WHEN: Calling find() on cached items
|
||||
$queryCount = 0;
|
||||
DB::listen(function () use (&$queryCount): void {
|
||||
$queryCount++;
|
||||
});
|
||||
|
||||
$brand1 = $directory->find(new ArcadiaId('1'));
|
||||
|
||||
// THEN
|
||||
expect($brand1)->not->toBeNull()
|
||||
->and($brand1?->name->value)->toBe('Le Gaulois')
|
||||
->and($queryCount)->toBe(0);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue