Query Caching
Query caching stores expensive database query results in memory, reducing database load and improving performance.
Basic Usage
remember(Duration)
Cache query results for a specified duration:
Future<void> rememberExample(DataSource dataSource) async {
// Cache for 5 minutes
final activeUsers = await dataSource
.query<$User>()
.whereEquals('active', true)
.orderBy('name')
.remember(Duration(minutes: 5))
.get();
// Cache for 1 hour
final statistics = await dataSource
.query<$User>()
.remember(Duration(hours: 1))
.get();
// Cache for 1 day
final users = await dataSource
.query<$User>()
.remember(Duration(days: 1))
.get();
}
How it works:
- First query executes against the database
- Results are stored in cache with the specified TTL
- Subsequent identical queries retrieve from cache
- Cache entry expires after the duration
rememberForever()
Cache results indefinitely (until manually cleared):
Future<void> rememberForeverExample(DataSource dataSource) async {
// Good for reference data that rarely changes
final users = await dataSource
.query<$User>()
.orderBy('name')
.rememberForever()
.get();
final activeUsers = await dataSource
.query<$User>()
.whereEquals('active', true)
.rememberForever()
.get();
}
warning
Use rememberForever() carefully! Cached data won't update automatically. Clear the cache manually when data changes.
dontRemember()
Bypass the cache for a specific query:
Future<void> dontRememberExample(DataSource dataSource) async {
final userId = 1;
final user = await dataSource.query<$User>().find(userId);
// Get fresh data, ignoring any cached results
final freshUser = await dataSource
.query<$User>()
.whereEquals('id', userId)
.dontRemember()
.first();
// Useful after updates
if (user != null) {
user.setAttribute('name', 'Updated');
await dataSource.repo<$User>().update(user);
final updated = await dataSource
.query<$User>()
.whereEquals('id', user.id)
.dontRemember()
.first();
}
}
Chaining with Other Methods
Future<void> cacheChainingExample(DataSource dataSource) async {
// Complex query with caching
final premiumUsers = await dataSource
.query<$User>()
.whereEquals('active', true)
.orderBy('name')
.limit(100)
.remember(Duration(minutes: 10))
.get();
// With eager loading
final postsWithAuthors = await dataSource
.query<$Post>()
.with_(['author'])
.whereEquals('published', true)
.remember(Duration(minutes: 15))
.get();
}
Cache Management
Flushing the Cache
Clear all cached queries:
Future<void> flushCacheExample(DataSource dataSource) async {
dataSource.context.flushQueryCache();
// Typically done after schema changes or deployment
// await runMigrations();
dataSource.context.flushQueryCache();
}
Vacuum Expired Entries
Remove expired cache entries to free memory:
Future<void> vacuumCacheExample(DataSource dataSource) async {
dataSource.context.vacuumQueryCache();
// Periodic vacuum
Timer.periodic(Duration(hours: 1), (_) {
dataSource.context.vacuumQueryCache();
});
}
Cache Statistics
Future<void> cacheStatsExample(DataSource dataSource) async {
final stats = dataSource.context.queryCacheStats;
print('Cache size: ${stats.size} entries');
print('Cache hits: ${stats.hits}');
print('Cache misses: ${stats.misses}');
print('Hit ratio: ${stats.hitRatio.toStringAsFixed(2)}%');
if (stats.hitRatio < 0.5) {
print('Warning: Low cache hit ratio: ${stats.hitRatio}');
}
}
Cache Events
Real-time notifications about cache operations:
| Event | Trigger | Properties |
|---|---|---|
CacheHitEvent | Result found in cache | sql, parameters, timestamp |
CacheMissEvent | Result not in cache | sql, parameters, timestamp |
CacheStoreEvent | Result stored in cache | sql, parameters, ttl, rowCount |
CacheForgetEvent | Entry removed | sql, parameters, timestamp |
CacheFlushEvent | All cache cleared | timestamp, entriesCleared |
CacheVacuumEvent | Expired entries cleaned | timestamp, entriesRemoved |
Listening to Events
void cacheEventsListeningExample(DataSource dataSource) {
final subscription = dataSource.context.queryCache.listen((event) {
print('${event.eventName}: ${event.sql}');
if (event is CacheHitEvent) {
print('✅ Cache hit!');
} else if (event is CacheMissEvent) {
print('❌ Cache miss');
} else if (event is CacheStoreEvent) {
print('💾 Stored ${event.rowCount} rows');
}
});
// Remove listener
dataSource.context.queryCache.unlisten(subscription);
}
Monitoring Cache Performance
class CacheMonitor {
int hits = 0;
int misses = 0;
void startMonitoring(QueryContext context) {
context.queryCache.listen((event) {
if (event is CacheHitEvent)
hits++;
else if (event is CacheMissEvent)
misses++;
});
}
double get hitRatio {
final total = hits + misses;
return total > 0 ? hits / total : 0.0;
}
void report() {
print('Cache Performance:');
print(' Hits: $hits');
print(' Misses: $misses');
print(' Hit Ratio: ${(hitRatio * 100).toStringAsFixed(2)}%');
}
}
Integration with Metrics
void cacheMetricsIntegration(DataSource dataSource, Metrics metrics) {
dataSource.context.queryCache.listen((event) {
if (event is CacheHitEvent) {
metrics.increment('cache.hits');
} else if (event is CacheMissEvent) {
metrics.increment('cache.misses');
} else if (event is CacheStoreEvent) {
metrics.histogram('cache.stored_rows', event.rowCount);
}
});
}
// Placeholder for metrics interface
abstract class Metrics {
void increment(String name);
void histogram(String name, int value);
}
When to Use Caching
✅ Good Candidates
- Dashboard statistics and reports
- Reference data (countries, categories)
- User permissions and roles
- Popular content (trending posts)
- Aggregated data
❌ Poor Candidates
- Real-time data requiring immediate consistency
- User-specific personalized content
- Rapidly changing data (live scores, stock prices)
- Write-heavy operations
Best Practices
Use Appropriate TTLs
Future<void> cacheBestPracticesTtl(DataSource dataSource) async {
// Short TTL for frequently changing data
final recentPosts = await dataSource
.query<$Post>()
.orderBy('createdAt', descending: true)
.limit(10)
.remember(Duration(minutes: 1))
.get();
// Longer TTL for stable data
final users = await dataSource
.query<$User>()
.remember(Duration(hours: 24))
.get();
}
Cache Expensive Queries
Future<void> cacheExpensiveQueries(DataSource dataSource) async {
// Complex aggregations
final stats = await dataSource
.query<$User>()
.selectRaw("DATE(created_at) as date, COUNT(*) as count")
.groupBy('date')
.remember(Duration(hours: 1))
.get();
}
Clear Cache on Schema Changes
Future<void> cacheClearOnSchema(DataSource dataSource) async {
// await runMigrations();
dataSource.context.flushQueryCache();
}
Don't Cache User-Specific Data Globally
Future<void> cacheUserSpecificBad(
DataSource dataSource,
int currentUserId,
) async {
// BAD - caches for all users!
final userPosts = await dataSource
.query<$Post>()
.whereEquals('user_id', currentUserId)
.remember(Duration(minutes: 5))
.get();
}
Future<void> cacheUserSpecificGood(
DataSource dataSource,
int currentUserId,
) async {
// BETTER - don't cache user-specific data
final userPosts = await dataSource
.query<$Post>()
.whereEquals('user_id', currentUserId)
.get();
}
Monitor Cache Performance
void cachePerformanceMonitor(DataSource dataSource) {
dataSource.context.queryCache.listen((event) {
if (event is CacheMissEvent) {
print('Cache miss for: ${event.sql}');
}
});
}
Cache Invalidation Strategies
Time-based (TTL)
Future<void> cacheTtlInvalidation(DataSource dataSource) async {
final data = await dataSource
.query<$User>()
.remember(Duration(minutes: 5))
.get();
}
Manual Invalidation
Future<void> cacheManualInvalidation(DataSource dataSource, User user) async {
await dataSource.repo<$User>().update(user);
dataSource.context.flushQueryCache();
}
Event-driven
void cacheEventDrivenInvalidation(DataSource dataSource) {
dataSource.context.onMutation((event) {
if (event.affectedModels.contains('User')) {
dataSource.context.flushQueryCache();
}
});
}
Performance Benefits
Query caching can provide:
- 10-100x faster query response times
- Reduced database load - Fewer connections and queries
- Better scalability - Handle more concurrent users
- Lower costs - Reduced database resource usage