Skip to main content

Multi-Database Support

Ormed supports connecting to multiple databases simultaneously for read replicas, tenant databases, or different database systems.

Configuring Multiple Connections

  // Primary database
final primaryDs = DataSource(
DataSourceOptions(
name: 'primary',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);

// Analytics database
final analyticsDs = DataSource(
DataSourceOptions(
name: 'analytics',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);

Using Named Connections

Create and initialize multiple named DataSources:

  final primaryDs = DataSource(
DataSourceOptions(
name: 'primary',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await primaryDs.init();

final analyticsDs = DataSource(
DataSourceOptions(
name: 'analytics',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await analyticsDs.init();

Transaction Caveat

Transactions cannot span multiple databases:

Coordinating Across Databases

Use compensating transactions for cross-database operations:

Transactions cannot span multiple databases. If you need cross-database workflows, coordinate explicitly and design for retries.

Multi-Tenant Architecture

class TenantDataSourceManager {
final Map<String, DataSource> _dataSources = {};

Future<DataSource> getForTenant(String tenantId) async {
if (!_dataSources.containsKey(tenantId)) {
final ds = DataSource(
DataSourceOptions(
name: 'tenant_$tenantId',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
tablePrefix: '${tenantId}_',
),
);
await ds.init();
_dataSources[tenantId] = ds;
}
return _dataSources[tenantId]!;
}

Future<void> dispose() async {
for (final ds in _dataSources.values) {
await ds.dispose();
}
_dataSources.clear();
}
}

Connection Factory

typedef DataSourceFactory = Future<DataSource> Function(String name);

DataSourceFactory createDataSourceFactory(
List<OrmModelDefinition> definitions,
) {
return (String name) async {
final ds = DataSource(
DataSourceOptions(
name: name,
driver: SqliteDriverAdapter.inMemory(),
entities: definitions,
),
);
await ds.init();
return ds;
};
}

ConnectionManager

class ConnectionManager {
final Map<String, DataSource> _connections = {};

Future<DataSource> get(String name) async {
if (!_connections.containsKey(name)) {
throw Exception('Connection "$name" not registered');
}
return _connections[name]!;
}

Future<void> register(String name, DataSource ds) async {
await ds.init();
_connections[name] = ds;
}

Future<void> disposeAll() async {
for (final ds in _connections.values) {
await ds.dispose();
}
_connections.clear();
}
}

Connection resolution for static helpers follows this order:

  1. A custom resolver registered via Model.bindConnectionResolver(...).
  2. The default connection in ConnectionManager (set by DataSource.init() or dataSource.setAsDefault()).

If you need per-request routing, bind a resolver and return the correct QueryContext:

Model.bindConnectionResolver(
resolveConnection: (name) => tenantResolver(name),
connectionManager: ConnectionManager.instance,
defaultConnection: 'primary',
);

Best Practices

  1. Name connections clearly - Use descriptive names like 'primary', 'analytics', 'cache'
  2. Set a default connection - Makes API cleaner for the most common case
  3. Avoid cross-database transactions - They're complex and often not supported
  4. Close connections properly - Use await dataSource.dispose() when shutting down
  5. Monitor connection health - Track active connections and query times

Driver Compatibility

FeatureSQLitePostgreSQLMySQL
Multiple connections
Read replicas🔜🔜🔜
Connection poolingDriver-specificDriver-specificDriver-specific
Cross-DB queries

🔜 = Planned for future release