Model Events
Ormed emits lifecycle events for every model operation:
saving, saved, creating, created, updating, updated, deleting, deleted, trashed, forceDeleted, restoring, restored, retrieved, and replicating.
- Cancellable:
saving,creating,updating,deleting,restoring,replicating - Soft delete aware:
trashed(soft delete),forceDeleted(hard delete) - Global listeners: subscribe on the shared
EventBus.instance
These snippets focus on event wiring and handlers. They assume you already have a DataSource or QueryContext unless the snippet explicitly shows setup.
Handler requirements
Handlers are static methods that take the event type you annotate with @OrmEvent.
Rules:
- Signature:
static void onXyz(ModelXyzEvent event) - Public/
staticonly; no return value - Use
event.cancel()on cancellable events to abort the operation
(table: 'audited_users')
class AuditedUser extends Model<AuditedUser> {
const AuditedUser({required this.id, required this.email});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String email;
(ModelSavingEvent)
static void onSaving(ModelSavingEvent event) =>
modelEventLog.add('saving ${event.attributes['email']}');
(ModelCreatedEvent)
static void onCreated(ModelCreatedEvent event) =>
modelEventLog.add('created ${event.model.id}');
(ModelForceDeletedEvent)
static void onForceDeleted(ModelForceDeletedEvent event) =>
modelEventLog.add('forceDeleted ${event.model.id}');
}
Listen globally
Attach listeners once when your app boots:
- Notes
- Code
- Register listeners once during app startup.
- Global listeners receive events from every
DataSource.
- saving / created
- updated / deleted
- retrieved
bus.on<ModelSavingEvent>((event) {
if (!_forUser(event.modelType)) return;
modelEventLog.add('saving ${event.attributes['email']}');
});
bus.on<ModelCreatedEvent>((event) {
if (!_forUser(event.modelType)) return;
modelEventLog.add('created ${event.model.id}');
});
bus.on<ModelUpdatedEvent>((event) {
if (!_forUser(event.modelType)) return;
modelEventLog.add('updated ${event.model.id}');
});
bus.on<ModelDeletedEvent>((event) {
if (!_forUser(event.modelType)) return;
final mode = event.forceDelete ? 'forceDelete' : 'trash';
modelEventLog.add('$mode ${event.model.id}');
});
bus.on<ModelRetrievedEvent>((event) {
if (!_forUser(event.modelType)) return;
modelEventLog.add('retrieved ${event.model.id}');
});
Guard operations (cancel)
Cancel dangerous deletes or updates in a listener by calling event.cancel().
Cancellation requires a model instance (so your handler can inspect fields). Use a query-based delete (delete() / deleteReturning()) or pass a tracked model to a repository method.
- Handler
- Usage
void enforceActiveUserDeletes(EventBus bus) {
bus.on<ModelDeletingEvent>((event) {
if (!_forUser(event.modelType)) return;
final user = event.model as EventUser;
if (!user.active) {
event.cancel(); // block delete/forceDelete
modelEventLog.add('blocked delete for inactive user ${user.id}');
}
});
}
Future<void> guardedDeleteExample(DataSource dataSource) async {
final inactive = await dataSource.repo<$EventUser>().insert(
$EventUser(id: 0, email: 'inactive@example.com', active: false),
);
final affected = await dataSource
.query<$EventUser>()
.whereEquals('id', inactive.id)
.delete();
// => 0 (delete is cancelled)
print(affected);
}
Full lifecycle walkthrough
- Setup
- Use / Verify
This wires listeners and creates a demo DataSource.
final bus = EventBus.instance;
registerModelEventListeners(bus);
enforceActiveUserDeletes(bus);
final dataSource = DataSource(
DataSourceOptions(
name: 'events-demo',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
),
);
await dataSource.init();
This performs insert/update/delete operations so you can observe event ordering.
final alice = await dataSource.repo<$EventUser>().insert(
$EventUser(
id: 0,
name: 'Alice',
email: 'alice@example.com',
active: true,
),
);
await dataSource.query<$EventUser>().whereEquals('id', alice.id).update({
'name': 'Alice Updated',
});
await dataSource
.query<$EventUser>()
.whereEquals('id', alice.id)
.deleteReturning();
await dataSource
.query<$EventUser>()
.withTrashed()
.whereEquals('id', alice.id)
.restore();
print(modelEventLog);
Event ordering
For a create + update + soft delete + restore flow, events fire in this order:
saving→creating→created→savedsaving→updating→updated→saveddeleting→deleted→trashed(soft delete)restoring→restored
Hard deletes emit deleting → deleted → forceDeleted.
Replication
ModelReplicatingEvent fires before Repository.replicate returns the copy. Cancel it to block replication or attach metadata to the new instance.
Suppressing Events
Sometimes you need to perform operations without triggering events—for example, during bulk imports, test seeding, or internal system updates.
Query-Level Suppression
Use withoutEvents() to suppress all model events for a query:
- Query
- Use Cases
Future<void> withoutEventsQuery(DataSource dataSource) async {
// Suppress events for any query operation
final query = dataSource.query<$UserPost>().withoutEvents();
// Insert without events
final post = await query.insertReturning({
'author_id': 1,
'title': 'No Events',
});
// Update without events
await dataSource
.query<$UserPost>()
.withoutEvents()
.whereEquals('id', post.id)
.update({'title': 'Updated Silently'});
// Delete without events
await dataSource
.query<$UserPost>()
.withoutEvents()
.whereEquals('id', post.id)
.delete();
}
Future<void> withoutEventsUseCase(DataSource dataSource) async {
// Use case: Bulk import where you want to skip audit logging
final importedPosts = <Map<String, Object?>>[];
for (var i = 0; i < 1000; i++) {
importedPosts.add({'author_id': 1, 'title': 'Import $i'});
}
// Skip events for performance during bulk operations
await dataSource.query<$UserPost>().withoutEvents().insertMany(importedPosts);
// Use case: Seeding test data without triggering side effects
await dataSource.query<$UserWithPosts>().withoutEvents().insertReturning({
'id': 999,
});
}
Relation-Level Suppression
Use createQuietlyRelation and createManyQuietlyRelation to create related models without events:
Future<void> createQuietlyRelation(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();
if (user != null) {
// Create without triggering model events (creating/created/saving/saved)
final post = await user.createQuietlyRelation<$UserPost>('posts', {
'title': 'Silent Post',
});
// Works with DTOs too
final dtoPost = await user.createQuietlyRelation<$UserPost>(
'posts',
UserPostInsertDto(title: 'Silent DTO Post'),
);
print('Quietly created: ${post.title}, ${dtoPost.title}');
}
}
- Bulk imports: Skip audit logging for performance
- Test seeding: Avoid triggering side effects in tests
- System migrations: Update data without user-facing events
- Internal sync: Replicate data without duplicate notifications