From a661ad796cafdd31e9a9ba724bb5207d044dad94 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:46:31 +0100 Subject: [PATCH] docs: update application architecture and database patterns for request-level caching best practices --- .ai/core/application-architecture.md | 16 ++++++--- .ai/patterns/database-patterns.md | 53 ++++++++++++++++++++++++++++ CLAUDE.md | 4 ++- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/.ai/core/application-architecture.md b/.ai/core/application-architecture.md index 64038d139..c1fe7c470 100644 --- a/.ai/core/application-architecture.md +++ b/.ai/core/application-architecture.md @@ -283,14 +283,22 @@ ### **Polymorphic Relationships** ### **Team-Based Soft Scoping** -All major resources include team-based query scoping: +All major resources include team-based query scoping with request-level caching: ```php -// Automatic team filtering -$applications = Application::ownedByCurrentTeam()->get(); -$servers = Server::ownedByCurrentTeam()->get(); +// ✅ CORRECT - Use cached methods (request-level cache via once()) +$applications = Application::ownedByCurrentTeamCached(); +$servers = Server::ownedByCurrentTeamCached(); + +// ✅ CORRECT - Filter cached collection in memory +$activeServers = Server::ownedByCurrentTeamCached()->where('is_active', true); + +// Only use query builder when you need eager loading or fresh data +$projects = Project::ownedByCurrentTeam()->with('environments')->get(); ``` +See [Database Patterns](.ai/patterns/database-patterns.md#request-level-caching-with-ownedbycurrentteamcached) for full documentation. + ### **Configuration Inheritance** Environment variables cascade from: diff --git a/.ai/patterns/database-patterns.md b/.ai/patterns/database-patterns.md index 1e40ea152..5a9d16f71 100644 --- a/.ai/patterns/database-patterns.md +++ b/.ai/patterns/database-patterns.md @@ -243,6 +243,59 @@ ### Database Indexes - **Composite indexes** for common queries - **Unique constraints** for business rules +### Request-Level Caching with ownedByCurrentTeamCached() + +Many models have both `ownedByCurrentTeam()` (returns query builder) and `ownedByCurrentTeamCached()` (returns cached collection). **Always prefer the cached version** to avoid duplicate database queries within the same request. + +**Models with cached methods available:** +- `Server`, `PrivateKey`, `Project` +- `Application` +- `StandalonePostgresql`, `StandaloneMysql`, `StandaloneRedis`, `StandaloneMariadb`, `StandaloneMongodb`, `StandaloneKeydb`, `StandaloneDragonfly`, `StandaloneClickhouse` +- `Service`, `ServiceApplication`, `ServiceDatabase` + +**Usage patterns:** +```php +// ✅ CORRECT - Uses request-level cache (via Laravel's once() helper) +$servers = Server::ownedByCurrentTeamCached(); + +// ❌ AVOID - Makes a new database query each time +$servers = Server::ownedByCurrentTeam()->get(); + +// ✅ CORRECT - Filter cached collection in memory +$activeServers = Server::ownedByCurrentTeamCached()->where('is_active', true); +$server = Server::ownedByCurrentTeamCached()->firstWhere('id', $serverId); +$serverIds = Server::ownedByCurrentTeamCached()->pluck('id'); + +// ❌ AVOID - Making filtered database queries when data is already cached +$activeServers = Server::ownedByCurrentTeam()->where('is_active', true)->get(); +``` + +**When to use which:** +- `ownedByCurrentTeamCached()` - **Default choice** for reading team data +- `ownedByCurrentTeam()` - Only when you need to chain query builder methods that can't be done on collections (like `with()` for eager loading), or when you explicitly need a fresh database query + +**Implementation pattern for new models:** +```php +/** + * Get query builder for resources owned by current team. + * If you need all resources without further query chaining, use ownedByCurrentTeamCached() instead. + */ +public static function ownedByCurrentTeam() +{ + return self::whereTeamId(currentTeam()->id); +} + +/** + * Get all resources owned by current team (cached for request duration). + */ +public static function ownedByCurrentTeamCached() +{ + return once(function () { + return self::ownedByCurrentTeam()->get(); + }); +} +``` + ## Data Consistency Patterns ### Database Transactions diff --git a/CLAUDE.md b/CLAUDE.md index b7c496e42..5cddb7fd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,6 +222,7 @@ ### Performance Considerations - Queue heavy operations - Optimize database queries with proper indexes - Use chunking for large data operations +- **CRITICAL**: Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` ### Code Style - Follow PSR-12 coding standards @@ -317,4 +318,5 @@ ### Livewire & Frontend Random other things you should remember: -- App\Models\Application::team must return a relationship instance., always use team() \ No newline at end of file +- App\Models\Application::team must return a relationship instance., always use team() +- Always use `Model::ownedByCurrentTeamCached()` instead of `Model::ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries \ No newline at end of file