Die nach Status gruppierte Zaehlung lief ueber accessibleBy() (user_id OR
company_id IN sub). Der OR-Ausdruck zwingt MySQL zu Full-Scans (EXPLAIN:
companies type=ALL, press_releases Full-Index-Scan ueber 124k Zeilen) und
war mit ~500-950ms der groesste Einzelposten der Seite.
Neue PressRelease::accessibleStatusCountsFor(User, ?Closure): kombiniert
Autor- und Firmen-Zweig als indexfreundliche UNION-Subquery (per PK
dedupliziert) und gruppiert darueber. Im Benchmark ~2,5x schneller bei
identischer Verteilung. Der Firmen-Filter der Uebersicht wird als geteilte
Closure auf Counts UND Liste angewandt (garantiert deckungsgleiche Filterung).
Gesamtmessung /admin/me/press-releases (User 14016, 26.772 Firmen / 20.830
PMs): Query-Pfad ~2850ms -> ~740ms warm; hydratisierte Company-Models 24.
Tests: accessibleStatusCountsFor zaehlt Autor+Firma je einmal (UNION-Dedup)
und wendet Zusatz-Constraints auf beide Zweige an.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- PressRelease::scopeOwnedByApiUser() (bewusst eng: nur user_id-Autorschaft,
anders als das breitere scopeAccessibleBy) ersetzt die byte-gleiche
findOwnedPressRelease-Query in PressReleaseController + ImageController.
- apiResourceRelations()-Helfer ersetzt den 5x identischen Eager-Load-Set
(company/category.translations/images) in Index/Show/Create/Update/Reclassify.
Bewusst NICHT umgesetzt: Slug-Reimplementierung (uniqueSlug) durch
HasUniqueSlug ersetzen - der API-Fallback Str::random(8) weicht vom
Trait-slugFallback() ab (Verhaltensaenderung bei leer-slugifizierten Titeln).
Tests: neuer Scope-Test; volle Suite gruen (802 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- User::currentPlan() und hasUnlimitedPressReleaseQuota() pro Request
memoisiert. Beide werden je Render mehrfach abgefragt (Status-Widget auf
jeder Seite, Buchungsseite, Quota-Methoden) und loesten bisher jeweils
eigene subscription-/Plan-/payment-option-Queries aus. fresh() setzt den
Cache zurueck.
- show.blade.php: getMyPR() war NICHT memoisiert und wurde pro Request bis
zu 7x mit voller Eager-Load-Query aufgerufen -> jetzt memoisiert. Firmen-
Kennzahlen direkt per withCount() eager geladen statt nachtraeglichem
loadCount().
- Dupliziertes Zugriffsmuster (withoutGlobalScopes + user_id/company_id) aus
edit/show/index in PressRelease::scopeAccessibleBy(User) zentralisiert
(Single Source of Truth fuer Kunden-PM-Zugriff), mit Tests abgesichert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>