Skip to main content

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.

Prerequisites

What You’ll Learn

  • Which model lifecycle events are emitted and cancellable
  • How to wire global and model-specific listeners
  • How to use events for auditing, guardrails, and side effects

Step Outcome

By the end of this page, you should be able to:

  • Attach listeners once at bootstrap

  • Cancel unsafe operations with guard handlers

  • Route lifecycle events into observability/audit systems

  • Cancellable: saving, creating, updating, deleting, restoring, replicating

  • Soft delete aware: trashed (soft delete), forceDeleted (hard delete)

  • Global listeners: subscribe on the shared EventBus.instance

Snippet context

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/static only; 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}');
}

Keep handlers small and side-effect-aware; push heavy work to background queues when needed.

Listen globally

Attach listeners once when your app boots:

  • Register listeners once during app startup.
  • Global listeners receive events from every DataSource.

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.

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}');
}
});
}

Full lifecycle walkthrough

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();

Event ordering

For a create + update + soft delete + restore flow, events fire in this order:

  1. savingcreatingcreatedsaved
  2. savingupdatingupdatedsaved
  3. deletingdeletedtrashed (soft delete)
  4. restoringrestored

Hard deletes emit deletingdeletedforceDeleted.

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:

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();
}

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}');
}
}
When to suppress events
  • 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

Verify Event Wiring

  1. Test one create/update/delete lifecycle sequence.
  2. Test one cancellable guard (event.cancel() path).
  3. Test one suppressed-events path (withoutEvents/quiet relations).

Read This Next