Start with the database
Code generation is optional. If you are bootstrapping a service, exploring an existing schema, or writing a small worker, connect to a driver and use the database facade directly. You can add generated models later without changing the underlying connection or query pipeline.
Install only what you need
For a local SQLite database:
dependencies:
ormed: ^0.3.0
ormed_sqlite: ^0.4.0
No build_runner, generated registry, or ormed_cli is required for this
path. See Drivers for PostgreSQL, MySQL/MariaDB, and D1
connection helpers.
Connect
Driver-specific helpers return the same OrmDatabase facade:
Future<OrmDatabase> connectWithoutCodegen() {
return SqliteDatabase.connect(path: ':memory:');
}
The facade owns the connection lifecycle. Close it when the process or request scope is finished:
final db = await SqliteDatabase.connect(path: 'database/app.sqlite');
try {
// use db
} finally {
await db.close();
}
Create a schema and run SQL
Use executeSchema for an immediate schema plan and executeRaw / queryRaw
when you already have SQL or need a database-specific statement:
Future<List<Map<String, Object?>>> directSchemaAndSql(OrmDatabase db) async {
await db.executeSchema((schema) {
schema.create('users', (table) {
table.id();
table.string('name');
table.boolean('active').defaultValue(true);
});
});
await db.executeRaw(
'INSERT INTO users (name, active) VALUES (?, ?)',
['Ada', true],
);
return db.queryRaw('SELECT id, name FROM users WHERE active = ?', [true]);
}
For changes that must be tracked and deployed repeatedly, use the migration
runner instead of treating executeSchema as a durable migration. The same
facade exposes migrate(...) when you want manually registered migration
entries without a generated registry.
Query a table without a model
db.table('users') returns Query<AdHocRow>. Rows are map-backed and support
the normal fluent filters, ordering, pagination, and streaming APIs:
Future<List<AdHocRow>> directTableQuery(OrmDatabase db) {
return db
.table('users')
.whereEquals('active', true)
.orderBy('name')
.get();
}
You do not need to describe every column up front. Add AdHocColumn metadata
only when you want explicit Dart type hints, a primary-key declaration, a
custom codec, or a column alias:
Future<List<AdHocRow>> directColumnMetadata(OrmDatabase db) {
return db
.table(
'users',
columns: const [
AdHocColumn(name: 'id', dartType: 'int', isPrimaryKey: true),
AdHocColumn(name: 'name', dartType: 'String'),
],
)
.get();
}
This keeps the common path lightweight while leaving a typed escape hatch for dynamic or legacy tables.
Transactions
The same handle wraps raw and table-builder work in one transaction:
Future<void> directTransaction(OrmDatabase db) async {
await db.transaction(() async {
await db.executeRaw(
'INSERT INTO users (name, active) VALUES (?, ?)',
['Grace', true],
);
await db.executeRaw(
'UPDATE users SET active = ? WHERE name = ?',
[false, 'Ada'],
);
});
}
Nested query-builder operations, raw SQL, schema plans, and migrations can all share the connection’s execution hooks and interceptors.
Add generated models later
When a table becomes part of your domain, introduce a model and registry:
- Add
ormed_cliandbuild_runneras development dependencies. - Define an annotated model and run
dart run build_runner build. - Pass the generated registry to
OrmDatabase.connect, or move to the generatedDataSourcescaffold.
The driver-first facade remains useful for raw SQL, ad-hoc tables, migrations, and database-specific maintenance even after code generation is enabled.
Next steps
- Query Builder for generated model queries and ad-hoc tables
- DataSource for lower-level lifecycle and routing
- Observability for query middleware and tracing
- Code Generation when your schema deserves domain types