Declare models topic
Define your models
Use ordinary Dart classes with annotations from package:orm/schema.dart.
Generation adds typed queries and an independent physical snapshot. Full reads
return the original class, so application methods and interfaces stay yours.
import 'package:orm/schema.dart';
@Model(table: 'users')
final class User({
@Id(generated: true) required final int id,
@Unique() required final String email,
required final String? nickname,
@DatabaseDefault(true) required final bool active,
});
@Model(table: 'posts')
@Index(['authorId', 'createdAt', 'id'], name: 'posts_author_timeline')
final class Post({
@Id(generated: true) required final int id,
@Relation(target: User, name: 'author', inverse: 'posts', onDelete: .cascade)
required final int authorId,
required final String title,
@ClientDefault(DateTime.now) required final DateTime createdAt,
});
Dart 3.13 primary constructors, shown above, and ordinary field declarations with an unnamed named-parameter constructor are supported. Annotate the field or its constructor parameter. Every persistent parameter must directly initialize its matching field. Models must be public, concrete and non-generic.
Run dart run orm generate lib/models.dart, or use
build_runner watch.
This produces models.orm.dart with the query API and exports of the original
User/Post classes, plus an independent models.snapshot.dart for migrations.
The company example
includes self references and a many-to-many association with business fields.
@Model() uses the class name exactly as the table name: User maps to User.
There is no case conversion or pluralization for tables. table: 'users' is an
explicit override. The query member uses the lower-camel class name, such as
db.user. Physical namespaces never change that member or the DTO class.
Column names default to snake_case; @Column(name: 'existing_column') overrides it.
Full reads, inserts and updates
final User created = await db.user.create(email: 'seven@example.com');
await db.user.byId(created.id).patch(nickname: 'Seven');
await db.user.byId(created.id).patch(nickname: null);
final List<String> titles = await db.user.byId(created.id)
.select((u) => u.posts.select((p) => p.title).many()).single();
Full reads supply every stored field to your constructor, including nullable, generated and computed fields. A constructor default is not a partial-row marker. Use explicit selections for partial results: scalars, typed Records or mapped DTOs.
Generated inserts have a separate contract:
- Non-null columns without defaults are required
- Nullable columns without defaults can be omitted and become SQL NULL
- Identities and defaulted fields accept literal values; omission keeps the default
- Computed columns are absent from generated inserts and patches
Generated userPatch(...) accepts literal values. Omission leaves a column unchanged;
nickname: null clears a nullable column. Use userPatch.values(...) for explicit
write intents, and userPatch.overlay([request, policy]) for ordered composition. Relations load only through explicit
selections. No relation placeholders, lazy queries, equality, serialization or
copyWith methods are injected into your model.
Mark nonpersistent instance fields with @Ignore(). An ignored constructor
parameter must be optional; a database read cannot invent a required value for it.
Mixins, interfaces and ordinary methods remain normal Dart behavior. They do not
implicitly register additional persistent fields or relations.
Scalar types and storage
| Dart field type | Inferred storage |
|---|---|
int, String, bool, double |
Integer, text, boolean, real |
BigInt, Decimal |
Exact integer, exact decimal |
DateTime |
UTC instant |
LocalDate, LocalTime, LocalDateTime |
Calendar values without a timezone |
Uint8List, SqlJson |
Bytes, JSON document |
| An enum | Text labels |
A domain value with @Column(codec: ...) |
The public const codec's storage |
Use nullable Dart types for SQL NULL. Import dart:typed_data for Uint8List.
SqlJson? distinguishes SQL NULL from a JSON null document. A custom codec can
supply storage for a domain type without changing the DTO's field type.
@Column(bits: 32), @Column(precision: 10, scale: 2) and temporal
@Column(precision: 3) declare storage limits. Each engine validates capabilities.
See types.
For stable enum labels independent of Dart constant renames:
enum Status { pending, done }
@Model()
final class Job({
@Id(generated: true) required final int id,
@Column(labels: {Status.pending: 'waiting', Status.done: 'complete'})
@DatabaseDefault(Status.pending)
required final Status status,
});
Every enum constant needs a distinct label. Labels are codecs over text storage; changing them needs an explicit data migration, not an inferred label rename.
Defaults, computed columns and checks
@DatabaseDefault(value) declares a typed SQL constant, including nullable null.
@DatabaseDefault.sql('CURRENT_TIMESTAMP') declares trusted SQL. @ClientDefault
references a public synchronous factory, called only for omitted insert values.
It can coexist with a database default: omission uses the client factory;
Use .databaseDefault() in the input factory's .values(...) call to request the database default.
A constant constructor default is a client-side fallback when no identity, database default or explicit client factory takes precedence. It creates no SQL DEFAULT and does not backfill historical rows. Generation never invokes your constructor, factory or codec. See defaults.
@Computed('price * quantity') marks a read-only computed scalar.
@Check('price >= 0', name: 'positive_price') adds a class-level database check.
Both accept sqlite, postgres, mysql and mariadb SQL overrides; computed
columns also accept storage: .stored or .virtual, subject to engine support.
Keys, indexes and relationships
@Id() marks a primary-key field. Multiple IDs form an ordered composite key in
constructor parameter order. @Id(generated: true) requires one non-null
integer-storage primary key. @Unique() marks a scalar; class-level
@Unique(['departmentId', 'email']) declares a composite key.
@Index(['departmentId', 'id'], name: 'employees_department_id') names an index.
A scalar @Relation(target: User, name: 'author') uses the annotated local field
and defaults to the target's primary key. Specify key: 'email' for an alternate
unique field. inverse: 'posts' adds reverse navigation to the target without
creating another foreign key or an implicit index.
Composite relations belong on the class. Ordered fields and keys lists pair
local and target Dart field names:
@Model()
@Relation(
target: Team,
name: 'team',
fields: ['tenantId', 'teamCode'],
keys: ['tenantId', 'code'],
inverse: 'members',
)
final class Member({
@Id() required final int id,
required final int tenantId,
required final String teamCode,
});
The target key must be primary or unique when constraint is true. Deletion
behavior defaults to restrict; choose cascade, setNull, setDefault or
noAction explicitly. constraint: false provides read-only navigation without
a foreign key, including navigation to non-unique fields. A unique foreign key
expresses a one-to-one constraint. Many-to-many associations use an explicit
model that can hold business fields.
Target classes are Dart symbols. Field lists and query names are string metadata:
the generator checks their existence, ordering, compatible types/codecs and
uniqueness before emission. They do not receive Dart member completion or automatic
analyzer symbol renames. Maintain these strings explicitly after a field rename,
then regenerate and analyze consumers. Preserve physical names with @Column
and @Model when refactoring only the Dart API.
Source discovery and static boundaries
A model source can be a file or a recursively discovered directory. Folder names never select a physical namespace. A sibling root file can also export models:
export 'employees.dart' show Employee;
export 'projects.dart' show Project, ProjectMember;
Generation includes root models and exports, then follows relation targets.
Unrelated imports are not additional roots. Generated .orm.dart and
.snapshot.dart files are excluded. Model sources must not import generated
clients; use independent libraries rather than part files. See
namespaces.
Metadata is resolved statically. Unsupported declarations fail with a source diagnostic; application constructors and factories are not executed to discover models. Generated snapshots contain physical metadata without application imports. Applied migration definitions stay frozen and destructive renames are never inferred.
Libraries
- schema Declare models
- Declares ordinary Dart model classes for static, typed ORM generation.