/ Query Cache

Query Cache

QueryCache caches database query results for improved performance. Reduce database load by caching frequently accessed data.


Query Cache Methods Summary

Method Description
enable() / disable()Toggle caching
get($key)Get cached result
set($key, $value, $ttl)Cache result
remember($key, $callback)Get or execute and cache
forget($key)Remove cached item
forgetByTag($tag)Remove items by tag
flush()Clear all cache

Basic Usage

Enable/Disable Caching

use Miko\Database\Cache\QueryCache;

// Enable caching
QueryCache::enable();

// Disable caching
QueryCache::disable();

// Check if enabled
if (QueryCache::isEnabled()) {
    echo "Caching is active";
}

Set Default TTL

// Set default TTL (time-to-live) in seconds
QueryCache::setDefaultTtl(300); // 5 minutes

Caching Queries

Remember Pattern (Recommended)

// Get from cache or execute query
$users = QueryCache::remember('active_users', function() {
    return User::where('IsActive', true)->get();
}, 300); // Cache for 5 minutes

// With tags for easy invalidation
$users = QueryCache::remember('active_users', function() {
    return User::where('IsActive', true)->get();
}, 300, ['users', 'active']);

Manual Get/Set

// Generate cache key
$key = QueryCache::generateKey($sql, $bindings);

// Try to get from cache
$result = QueryCache::get($key);

if ($result === null) {
    // Execute query
    $result = User::all();
    
    // Store in cache
    QueryCache::set($key, $result, 300, ['users']);
}

Cache Invalidation

Forget Specific Key

// Remove specific cached item
QueryCache::forget('active_users');

Forget by Tag

// Remove all items with specific tag
QueryCache::forgetByTag('users');

// Useful after data changes
User::saved(function() {
    QueryCache::forgetByTag('users');
});

Forget by Table

// Remove all cache entries for a table
QueryCache::forgetTable('users');

Clear All Cache

// Remove all cached queries
QueryCache::flush();

Tags

Tags allow grouping related cache entries for easy invalidation.

Cache with Tags

// Cache with multiple tags
QueryCache::set('user_123', $userData, 300, ['users', 'user_123']);
QueryCache::set('user_123_orders', $orders, 300, ['users', 'orders', 'user_123']);
QueryCache::set('user_123_profile', $profile, 300, ['users', 'profiles', 'user_123']);

Invalidate by Tag

// When user is updated, invalidate all their cache
QueryCache::forgetByTag('user_123');

// When any user changes, invalidate all user cache
QueryCache::forgetByTag('users');

Statistics

$stats = QueryCache::getStats();

print_r($stats);
// [
//     'entries' => 150,
//     'max_size' => 1000,
//     'total_hits' => 5000,
//     'total_misses' => 500,
//     'hit_rate' => 90.9,
//     'expired' => 10,
//     'tags' => 25,
//     'enabled' => true
// ]

// Hit rate
echo "Cache hit rate: " . $stats['hit_rate'] . "%";

Configuration

Max Cache Size

// Set maximum number of cached entries
QueryCache::setMaxSize(1000);

// LRU (Least Recently Used) eviction happens automatically

Cleanup Expired

// Manually cleanup expired entries
QueryCache::cleanupExpired();

// Usually not needed - happens automatically

Practical Examples

Caching Model Queries

class User extends Model
{
    public static function getActive(): array
    {
        return QueryCache::remember('users:active', function() {
            return static::where('IsActive', true)
                ->orderBy('Name')
                ->get();
        }, 300, ['users']);
    }
    
    public static function findCached(int $id): ?User
    {
        return QueryCache::remember("user:{$id}", function() use ($id) {
            return static::find($id);
        }, 600, ['users', "user:{$id}"]);
    }
    
    public static function getTopSellers(int $limit = 10): array
    {
        return QueryCache::remember("users:top_sellers:{$limit}", function() use ($limit) {
            return static::query()
                ->select('users.*')
                ->selectRaw('SUM(orders.TotalAmount) as total_sales')
                ->join('orders', 'users.Id', '=', 'orders.UserId')
                ->groupBy('users.Id')
                ->orderBy('total_sales', 'desc')
                ->take($limit)
                ->get();
        }, 3600, ['users', 'orders', 'reports']);
    }
}

// Usage
$activeUsers = User::getActive();
$user = User::findCached(123);
$topSellers = User::getTopSellers(10);

Cache Invalidation on Model Events

use Miko\Database\ORM\Observer;

class UserCacheObserver extends Observer
{
    public function saved(Model $model): void
    {
        // Invalidate user-specific cache
        QueryCache::forgetByTag("user:{$model->Id}");
        
        // Invalidate list caches
        QueryCache::forget('users:active');
    }
    
    public function deleted(Model $model): void
    {
        QueryCache::forgetByTag("user:{$model->Id}");
        QueryCache::forgetByTag('users');
    }
}

// Register observer
ObserverManager::register(User::class, UserCacheObserver::class);

API Response Caching

class ProductController
{
    public function index(): void
    {
        $page = (int)($_GET['page'] ?? 1);
        $perPage = (int)($_GET['per_page'] ?? 20);
        
        $cacheKey = "products:list:page_{$page}:per_{$perPage}";
        
        $data = QueryCache::remember($cacheKey, function() use ($page, $perPage) {
            $result = Product::where('IsActive', true)
                ->orderBy('Name')
                ->paginate($perPage, $page);
            
            return [
                'items' => $result['data'],
                'pagination' => [
                    'current_page' => $result['current_page'],
                    'last_page' => $result['last_page'],
                    'total' => $result['total']
                ]
            ];
        }, 300, ['products']);
        
        JsonResponse::success($data);
    }
    
    public function show(int $id): void
    {
        $product = QueryCache::remember("product:{$id}", function() use ($id) {
            return Product::with('category', 'images')->find($id);
        }, 600, ['products', "product:{$id}"]);
        
        if (!$product) {
            JsonResponse::notFound('Product not found');
            return;
        }
        
        JsonResponse::success($product);
    }
}

Dashboard Statistics

class DashboardService
{
    public function getStats(): array
    {
        return [
            'users' => $this->getUserStats(),
            'orders' => $this->getOrderStats(),
            'revenue' => $this->getRevenueStats()
        ];
    }
    
    private function getUserStats(): array
    {
        return QueryCache::remember('dashboard:user_stats', function() {
            return [
                'total' => User::count(),
                'active' => User::where('IsActive', true)->count(),
                'new_today' => User::where('CreatedDate', '>=', date('Y-m-d'))->count()
            ];
        }, 300, ['dashboard', 'users']);
    }
    
    private function getOrderStats(): array
    {
        return QueryCache::remember('dashboard:order_stats', function() {
            return [
                'total' => Order::count(),
                'pending' => Order::where('Status', 'pending')->count(),
                'completed' => Order::where('Status', 'completed')->count(),
                'today' => Order::where('CreatedDate', '>=', date('Y-m-d'))->count()
            ];
        }, 60, ['dashboard', 'orders']); // Shorter TTL for orders
    }
    
    private function getRevenueStats(): array
    {
        return QueryCache::remember('dashboard:revenue_stats', function() {
            return [
                'today' => Order::where('CreatedDate', '>=', date('Y-m-d'))
                    ->where('Status', 'completed')
                    ->sum('TotalAmount'),
                'this_month' => Order::where('CreatedDate', '>=', date('Y-m-01'))
                    ->where('Status', 'completed')
                    ->sum('TotalAmount'),
                'this_year' => Order::where('CreatedDate', '>=', date('Y-01-01'))
                    ->where('Status', 'completed')
                    ->sum('TotalAmount')
            ];
        }, 300, ['dashboard', 'orders', 'revenue']);
    }
}

Best Practices

Practice Description
Meaningful cache keysInclude relevant identifiers in key names
Use tagsMakes cache invalidation easier
Set appropriate TTLBalance freshness vs performance
Invalidate on writesUse observers or events to clear cache
Monitor hit rateAdjust strategy based on performance
// Good: Descriptive key with tags
QueryCache::remember('users:active:role_admin', $callback, 300, ['users', 'admins']);

// Bad: Generic key without tags
QueryCache::remember('data', $callback, 300);