Skip to main content

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:

  1. First query executes against the database
  2. Results are stored in cache with the specified TTL
  3. Subsequent identical queries retrieve from cache
  4. 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:

EventTriggerProperties
CacheHitEventResult found in cachesql, parameters, timestamp
CacheMissEventResult not in cachesql, parameters, timestamp
CacheStoreEventResult stored in cachesql, parameters, ttl, rowCount
CacheForgetEventEntry removedsql, parameters, timestamp
CacheFlushEventAll cache clearedtimestamp, entriesCleared
CacheVacuumEventExpired entries cleanedtimestamp, 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