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.

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

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