Skip to content

Database and cache

import 'package:gisila/gisila.dart';

gisila has no database driver of its own. It defines the per-request session lifecycle; a provider connects it to your driver or ORM. For gisila_orm, package:gisila_orm/web.dart ships a ready provider and error mapper (see Use with gisila_orm).

Acquires and releases per-request database sessions.

abstract class DatabaseProvider<S> {
const DatabaseProvider();
Future<S> open(Request request);
Future<void> close(S session, {required bool ok});
Future<void> dispose() async {}
}

When to use: set one as AppConfig.database whenever handlers use a database, so they get it with ctx.db<S>() and so it is closed on shutdown. Implement it yourself for drivers other than gisila_orm, or to wrap each request in a transaction.

Method Description
open(request) Called once per request, before your middleware and route. Return a pooled connection, a shared pool handle or a new transaction. If it throws, the request gets a normal JSON error.
close(session, ok:) Called after the response is produced. ok is true when the status is below 500 (a thrown HttpException under 500, a form validation error and a WebSocket upgrade count as ok; any other exception does not). Commit on ok, roll back otherwise. Exceptions thrown here are swallowed.
dispose() Called by app.stop() after in-flight requests finish. Close the pool here.

open runs for every request that gets past CORS, rate limiting and the body cap, including requests for unknown routes and static files. If you have expensive sessions (one transaction per request), keep open cheap or return a lazily opened wrapper.

When a request times out, the handler may still be using the session. The middleware waits for it to finish and then calls close(ok: false).

Example

/// One transaction per request, committed on success.
class TxProvider extends DatabaseProvider<Tx> {
TxProvider(this.pool);
final Pool pool;
@override
Future<Tx> open(Request request) => pool.begin();
@override
Future<void> close(Tx tx, {required bool ok}) => ok ? tx.commit() : tx.rollback();
@override
Future<void> dispose() => pool.close();
}
// Handlers:
final tx = ctx.db<Tx>();

Translates driver errors into HttpExceptions.

abstract class DbErrorMapper {
const DbErrorMapper();
HttpException? map(Object error, StackTrace stack);
}

When to use: so a unique-constraint violation becomes 409 and a foreign-key violation 400 instead of a 500, without try/catch in every service. Set it as AppConfig.dbErrorMapper. Return null for errors you don’t recognise. Never echo raw driver messages: they can contain other users’ data.

class MyDbErrorMapper extends DbErrorMapper {
const MyDbErrorMapper();
@override
HttpException? map(Object error, StackTrace stack) {
if (error is UniqueViolation) {
return Conflict('Already exists', code: 'unique_violation');
}
return null;
}
}

For Postgres through gisila_orm, use PostgresDbErrorMapper from package:gisila_orm/web.dart.

const String kDbKey = 'gisila.db.session';

The RequestContext.extra key holding the session. Use ctx.db<T>() instead.

A pluggable key/value cache shared by every request.

abstract class CacheProvider {
const CacheProvider();
Future<void> connect() async {}
Future<void> disconnect() async {}
Future<String?> get(String key);
Future<void> set(String key, String value, {Duration? ttl});
Future<bool> delete(String key);
Future<bool> exists(String key);
Future<List<String>> keys(String pattern);
Future<void> flush();
Future<int> increment(String key, {int by = 1});
Future<int> decrement(String key, {int by = 1});
Future<Map<String, String>> mget(List<String> keys);
Future<void> mset(Map<String, String> entries, {Duration? ttl});
Future<int> mdelete(List<String> keys);
}

When to use: caching expensive reads, storing sessions, counters, idempotency keys. Set it as AppConfig.cache and use ctx.cache in handlers and services. Values are strings: encode structured data with jsonEncode. For a production cache shared across processes, use RedisCacheProvider from gisila_redis; implement CacheProvider for other backends.

Method Description
connect() / disconnect() Called by app.init() (before the server accepts connections) and app.stop(). No-ops by default.
get(key) Value, or null when absent or expired.
set(key, value, ttl:) Store; ttl: null means no expiry.
delete(key) true when the key existed.
exists(key) true when present and not expired.
keys(pattern) Keys matching a glob (*, ?). Backends that can’t enumerate may return [].
flush() Remove everything. For tests and admin tools.
increment / decrement Atomic counter; a missing key counts as 0. FormatException when the value is not an integer.
mget, mset, mdelete Multi-key helpers. Default implementations call the single-key methods in parallel; backends with native commands override them.

Example

class ProductService extends Service {
Future<Product> get(String id) async {
final cached = await ctx.cache.get('product:$id');
if (cached != null) return Product.fromJson(jsonDecode(cached) as Map<String, dynamic>);
final product = await _load(id);
await ctx.cache.set('product:$id', jsonEncode(product), ttl: const Duration(minutes: 5));
return product;
}
}
class InMemoryCacheProvider extends CacheProvider {
InMemoryCacheProvider({int? maxEntries, Duration? defaultTtl});
final int? maxEntries;
final Duration? defaultTtl;
int get length;
}

A cache in a Dart Map. Each isolate and process has its own copy.

When to use: development, tests and single-instance deployments. Not for several instances, where each would cache (and count) separately.

Field Description
maxEntries When full, the oldest entry (by insertion) is evicted before a new key is added. null is unlimited.
defaultTtl TTL for set calls without one. null means no expiry.
length Number of live entries (expired ones are evicted first).

increment here treats a non-integer value as 0 rather than throwing, and each increment re-stores the key with defaultTtl, so a counter’s expiry restarts on every hit.

AppConfig(cache: InMemoryCacheProvider(maxEntries: 10000, defaultTtl: const Duration(minutes: 15)))
const String kCacheKey = 'gisila.cache';

The RequestContext.extra key holding the cache. Use ctx.cache instead.

See also: Use with gisila_orm, ORM documentation.