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).
Database
Section titled “Database”DatabaseProvider
Section titled “DatabaseProvider”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>();DbErrorMapper
Section titled “DbErrorMapper”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.
kDbKey
Section titled “kDbKey”const String kDbKey = 'gisila.db.session';The RequestContext.extra key holding the session. Use ctx.db<T>() instead.
CacheProvider
Section titled “CacheProvider”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; }}InMemoryCacheProvider
Section titled “InMemoryCacheProvider”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)))kCacheKey
Section titled “kCacheKey”const String kCacheKey = 'gisila.cache';The RequestContext.extra key holding the cache. Use ctx.cache instead.
See also: Use with gisila_orm, ORM documentation.