Perf-Fix: User::accessibleCompanyIds() hydratisiert keine Company-Models mehr
Auf /admin/me/press-releases wurden bei einem User mit sehr vielen Firmen ~107.000 Company-Models geladen. Ursache: accessibleCompanyIds() nutzte das Eloquent-Relation-pluck() (ownedCompanies()->pluck() / companies()-> pluck()), das pro ID ein vollstaendiges Model hydratisiert. Die Methode wird ueber PressRelease::scopeAccessibleBy() pro Seitenaufruf ~2x ausgewertet (Status-Count- + Listen-Query) -> ~2x 53k Models. Fix: - toBase()->pluck() statt Relation-pluck(): liefert nur die ID-Werte ohne Hydration, wendet die Global Scopes (PortalScope) weiterhin an. - Ergebnis pro Request memoisiert (cachedAccessibleCompanyIds), da der Scope die Methode mehrfach aufruft. Messung (User 14016, 26.772 Firmen): 107.238 -> 24 hydratisierte Company-Models auf der PM-Liste. Ergebnis-IDs unveraendert. Regressionstests: accessibleCompanyIds liefert owned+member ohne Dubletten und hydratisiert 0 Company-Models. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a30b13187
commit
38884a53d6
2 changed files with 43 additions and 2 deletions
|
|
@ -132,6 +132,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
|
||||
private ?int $cachedOpenSinglePurchaseCount = null;
|
||||
|
||||
/**
|
||||
* @var list<int>|null
|
||||
*/
|
||||
private ?array $cachedAccessibleCompanyIds = null;
|
||||
|
||||
/**
|
||||
* Findet einen User case-insensitive anhand der E-Mail. Single Source of
|
||||
* Truth für die E-Mail-basierte Auflösung (Magic-Link, Social-Login,
|
||||
|
|
@ -452,9 +457,19 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
*/
|
||||
public function accessibleCompanyIds(): array
|
||||
{
|
||||
return $this->ownedCompanies()->pluck('companies.id')
|
||||
->merge($this->companies()->pluck('companies.id'))
|
||||
if ($this->cachedAccessibleCompanyIds !== null) {
|
||||
return $this->cachedAccessibleCompanyIds;
|
||||
}
|
||||
|
||||
// toBase() statt der Relation-pluck(): Eloquents Builder::pluck()
|
||||
// hydratisiert sonst pro ID ein vollständiges Company-Model (bei
|
||||
// Power-Usern mit zehntausenden Firmen ein massives Speicher-/Zeit-Leck).
|
||||
// toBase() wendet die Global Scopes weiter an, liefert aber nur die
|
||||
// nackten ID-Werte ohne Model-Hydration.
|
||||
return $this->cachedAccessibleCompanyIds = $this->ownedCompanies()->toBase()->pluck('companies.id')
|
||||
->merge($this->companies()->toBase()->pluck('companies.id'))
|
||||
->unique()
|
||||
->map(fn ($id): int => (int) $id)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,32 @@ test('accessibleBy excludes press releases of strangers and unrelated companies'
|
|||
expect($ids)->not->toContain($foreignPr->id);
|
||||
});
|
||||
|
||||
test('accessibleCompanyIds returns owned and member company ids without duplicates', function () {
|
||||
$user = User::factory()->create();
|
||||
$owned = Company::factory()->create(['owner_user_id' => $user->id]);
|
||||
$member = Company::factory()->create();
|
||||
$member->users()->attach($user->id, ['role' => 'member']);
|
||||
Company::factory()->create(); // fremde Firma
|
||||
|
||||
$ids = $user->accessibleCompanyIds();
|
||||
|
||||
expect($ids)->toEqualCanonicalizing([$owned->id, $member->id]);
|
||||
});
|
||||
|
||||
test('accessibleCompanyIds does not hydrate Company models', function () {
|
||||
$user = User::factory()->create();
|
||||
Company::factory()->count(3)->create(['owner_user_id' => $user->id]);
|
||||
|
||||
$hydrated = 0;
|
||||
Company::retrieved(function () use (&$hydrated): void {
|
||||
$hydrated++;
|
||||
});
|
||||
|
||||
$user->accessibleCompanyIds();
|
||||
|
||||
expect($hydrated)->toBe(0);
|
||||
});
|
||||
|
||||
test('accessibleBy bypasses the portal global scope so own PMs are visible cross-portal', function () {
|
||||
$user = User::factory()->create();
|
||||
$pr = PressRelease::factory()->create([
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue