Model events
A model fires events when you save, delete or restore it. You can run your own code on these events. There are two ways: hooks in the model, and observer classes.
Hooks in the model
Override an on... method in the model. Then run dart run build_runner build.
@DoxModel(softDelete: true)
class User extends UserGenerator {
@Column()
String? email;
@override
Future<void> onCreating() async {
email = email?.trim().toLowerCase();
}
@override
Future<void> onDeleted() async {
await AuditLog().create({'message': 'user $id deleted'});
}
}
Info
The hook names start with on (onCreating, not creating).
This is because created and deleted are common column names.
Declare the hooks in the model class itself. The generator looks there.
Events and order
| Call | Events, in order |
|---|---|
save() on a new model |
saving → creating → INSERT → created → saved |
save() on a loaded model |
saving → updating → UPDATE → updated → saved |
create({...}) |
the same as save() on a new model |
delete() |
deleting → DELETE or soft delete → deleted |
forceDelete() |
forceDeleting → deleting → DELETE → deleted → forceDeleted |
restore() |
restoring → UPDATE deleted_at = NULL → restored |
The model hook runs first. Then the observers run, in the order you added them.
Query updates and deletes fire no events
Events fire only on a model instance.
User().where('active', false).update({...}), User().where(...).delete()
and insertMultiple() run one query and fire no events. This is the same as Laravel.
Cancel an operation
Throw CancelModelEvent in a "before" event
(saving, creating, updating, deleting, forceDeleting, restoring).
The query does not run. save(), delete(), forceDelete() and restore() return false.
@override
Future<void> onDeleting() async {
if (isAdmin == true) throw CancelModelEvent('admins can not be deleted');
}
create({...}) returns the unsaved model (its id is null) when an event cancels it.
Any other error from a hook is thrown to the caller.
Observers
An observer is a class that listens to one model. Override only the methods you need. Each method gets the model.
class UserObserver extends ModelObserver<User> {
final Mailer mailer;
UserObserver(this.mailer);
@override
Future<void> created(User user) async {
await mailer.sendWelcome(user.email!);
}
}
Add it once, for example when the app starts:
- You can add more than one observer to a model.
Model.forgetObservers<User>()removes the observers ofUser.Model.forgetObservers()removes all.
Skip events
await user.saveQuietly(); // save without events
await Model.withoutEvents(() async {
await user.save();
await post.delete();
});
Transactions
Hooks run inside the transaction of the caller.
save() does not start a transaction by itself.
- If an "after" hook (
created,saved, ...) throws, the row is already written. - To undo the row when a hook fails, wrap the call in
QueryBuilder.transaction():
await QueryBuilder.transaction(() async {
await user.save(); // if onCreated() throws, the INSERT is rolled back
});
Performance
- There is no reflection. The generator sets
hasModelHooksonly for models that declare a hook. - A model with no hooks and no observers does no event work. It is one
boolcheck, plus one map lookup when some other model has observers. - When a model has events,
create({...})fills a model withfromMap()and callssave(). Only the@Columnfields are saved then.