Skip to main content

Query Caching

Query caching stores query results in memory so repeated reads can skip database round-trips.

1. Start with remember​

Cache query results for a specified TTL:

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. Equivalent subsequent queries return cached rows
  4. Cache entry expires after the duration

Use rememberForever() sparingly​

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.

Use dontRemember() to force fresh reads​

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();
}
}

Cache works with normal query chaining​

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();
}

2. Manage Cache Lifecycle​

Flush all cache entries​

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();
});
}

Inspect 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}');
}
}

3. Observe Cache Behavior​

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

Listen 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);
}
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)}%');
}
}

Integrate with metrics backends​

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);
}

4. Choose Good Cache Targets​

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

5. Apply Practical Patterns​

Select TTLs by volatility​

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();
}

Prioritize 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 after schema/data-shape changes​

Future<void> cacheClearOnSchema(DataSource dataSource) async {
// await runMigrations();
dataSource.context.flushQueryCache();
}

Avoid global caching for user-specific data​

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}');
}
});
}

6. Pick an Invalidation Strategy​

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 invalidation​

void cacheEventDrivenInvalidation(DataSource dataSource) {
dataSource.context.onMutation((event) {
if (event.affectedModels.contains('User')) {
dataSource.context.flushQueryCache();
}
});
}

Verify Your Setup​

  • Re-running the same read query produces cache hits.
  • Write paths invalidate cache intentionally (flushQueryCache or event-driven hook).
  • Hit ratio is monitored so cache usage can be tuned over time.

Read This Next​