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 keys | Include relevant identifiers in key names |
| Use tags | Makes cache invalidation easier |
| Set appropriate TTL | Balance freshness vs performance |
| Invalidate on writes | Use observers or events to clear cache |
| Monitor hit rate | Adjust 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);