docs/versioned_docs/version-7.0/events.md
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
There are two ways to hook into the lifecycle of an entity:
Both approaches support the same events. Hooks are executed before subscribers.
| Event | When it fires |
|---|---|
onInit | When an entity instance is created (via em.create() or when loaded from database) |
onLoad | When an entity is fully loaded from the database (not for references) |
beforeCreate | Before a new entity is inserted into the database |
afterCreate | After a new entity is inserted and merged into the identity map |
beforeUpdate | Before an existing entity is updated in the database |
afterUpdate | After an entity is updated and changes are merged |
beforeUpsert | Before em.upsert() or em.upsertMany() executes |
afterUpsert | After upsert completes, receives the managed entity |
beforeDelete | Before an entity is deleted from the database |
afterDelete | After an entity is deleted and removed from identity map |
<Tabs groupId="entity-def" defaultValue="define-entity-class" values={[ {label: 'defineEntity + class', value: 'define-entity-class'}, {label: 'defineEntity', value: 'define-entity'}, {label: 'reflect-metadata', value: 'reflect-metadata'}, {label: 'ts-morph', value: 'ts-morph'}, ]}
<TabItem value="define-entity-class">
With defineEntity + class, use the addHook method to register hooks after the class is defined:
import { defineEntity, type EventArgs, p } from '@mikro-orm/core';
const ArticleSchema = defineEntity({
name: 'Article',
properties: {
id: p.integer().primary(),
title: p.string(),
slug: p.string().unique(),
updatedAt: p.datetime(),
},
});
export class Article extends ArticleSchema.class {}
ArticleSchema.setClass(Article);
// highlight-start
ArticleSchema.addHook('beforeCreate', async (args: EventArgs<Article>) => {
const article = args.entity;
if (!article.slug) {
article.slug = article.title.toLowerCase().replace(/\s+/g, '-');
}
});
ArticleSchema.addHook('beforeUpdate', async (args: EventArgs<Article>) => {
args.entity.updatedAt = new Date();
});
// highlight-end
</TabItem> <TabItem value="define-entity">You can also pass hooks inline via the
hooksproperty in thedefineEntitycall, butargs.entitywill be typed asanythere because the entity type is not yet known. Explicitly typing the parameter (e.g.EventArgs<Article>) won't work either, as it would create a circular reference. UseaddHookafter the class is defined to get full type safety.
With defineEntity (no class), use the addHook method to register hooks after the entity is defined:
import { defineEntity, type InferEntity, type EventArgs, p } from '@mikro-orm/core';
export const Article = defineEntity({
name: 'Article',
properties: {
id: p.integer().primary(),
title: p.string(),
slug: p.string().unique(),
updatedAt: p.datetime(),
},
});
export type IArticle = InferEntity<typeof Article>;
// highlight-start
Article.addHook('beforeCreate', async (args: EventArgs<IArticle>) => {
const article = args.entity;
if (!article.slug) {
article.slug = article.title.toLowerCase().replace(/\s+/g, '-');
}
});
Article.addHook('beforeUpdate', async (args: EventArgs<IArticle>) => {
args.entity.updatedAt = new Date();
});
// highlight-end
</TabItem> <TabItem value="reflect-metadata">You can also pass hooks inline via the
hooksproperty in thedefineEntitycall, butargs.entitywill be typed asanythere because the entity type is not yet known. Explicitly typing the parameter (e.g.EventArgs<IArticle>) won't work either, as it would create a circular reference. UseaddHookafter the entity and its type alias are defined to get full type safety.
With decorators, mark entity methods with hook decorators like @BeforeCreate(), @BeforeUpdate(), etc.:
import { Entity, PrimaryKey, Property, BeforeCreate, BeforeUpdate } from '@mikro-orm/core';
@Entity()
export class Article {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ unique: true })
slug!: string;
@Property()
updatedAt?: Date;
@BeforeCreate()
generateSlug() {
if (!this.slug) {
this.slug = this.title.toLowerCase().replace(/\s+/g, '-');
}
}
@BeforeUpdate()
updateTimestamp() {
this.updatedAt = new Date();
}
}
Multiple methods can have the same hook decorator. Inside hook methods, this refers to the entity instance.
With decorators, mark entity methods with hook decorators like @BeforeCreate(), @BeforeUpdate(), etc.:
import { Entity, PrimaryKey, Property, BeforeCreate, BeforeUpdate } from '@mikro-orm/core';
@Entity()
export class Article {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ unique: true })
slug!: string;
@Property()
updatedAt?: Date;
@BeforeCreate()
generateSlug() {
if (!this.slug) {
this.slug = this.title.toLowerCase().replace(/\s+/g, '-');
}
}
@BeforeUpdate()
updateTimestamp() {
this.updatedAt = new Date();
}
}
Multiple methods can have the same hook decorator. Inside hook methods, this refers to the entity instance.
All hooks receive an EventArgs object and can be async (except onInit):
async function myHook(args: EventArgs<MyEntity>): Promise<void> {
const entity = args.entity; // the entity instance
const em = args.em; // the EntityManager
const changeSet = args.changeSet; // available during flush (create/update/delete)
}
onInit:
em.create() or loaded from databasenew Entity() directlywrap(this).isInitialized() to distinguishonLoad:
beforeUpdate / afterUpdate:
Since em.upsert() doesn't know if the operation will be an insert or update, it has dedicated hooks:
beforeUpsert - may receive a DTO instead of entity instanceafterUpsert - always receives the managed entity instanceUse EventArgs.meta to identify the entity type when receiving a DTO.
The beforeUpdate/afterUpdate hooks fire when an UPDATE query is generated. This only happens for changes to:
Collection changes don't trigger update events because:
To observe collection changes during flush, use uow.getCollectionUpdates() in a flush event subscriber.
Hooks execute inside the Unit of Work commit phase, after change sets are computed:
em.flush() - throws a validation errorem.persist() - can cause undefined behaviorbeforeFlush event instead (see Flush Events)Use EventSubscriber when you want to:
EventArgs including change setsRegister globally in the ORM config:
MikroORM.init({
subscribers: [new ArticleSubscriber(), new AuditSubscriber()],
});
Or dynamically at runtime:
em.getEventManager().registerSubscriber(new ArticleSubscriber());
import { EventSubscriber, EventArgs } from '@mikro-orm/core';
import { Article } from './entities/Article.js';
export class ArticleSubscriber implements EventSubscriber<Article> {
// Only subscribe to Article events
getSubscribedEntities() {
return [Article];
}
async beforeCreate(args: EventArgs<Article>) {
console.log('Creating article:', args.entity.title);
}
async afterUpdate(args: EventArgs<Article>) {
// args.changeSet contains the changes
console.log('Updated fields:', Object.keys(args.changeSet?.payload ?? {}));
}
}
Omit getSubscribedEntities() to subscribe to all entities:
export class AuditSubscriber implements EventSubscriber {
async afterCreate(args: EventArgs<unknown>) {
console.log('Created:', args.changeSet?.name, args.changeSet?.entity);
}
}
import { EventArgs, FlushEventArgs, TransactionEventArgs, EventSubscriber } from '@mikro-orm/core';
export class FullSubscriber implements EventSubscriber {
// Entity lifecycle events
onInit<T>(args: EventArgs<T>): void { }
async onLoad<T>(args: EventArgs<T>): Promise<void> { }
async beforeCreate<T>(args: EventArgs<T>): Promise<void> { }
async afterCreate<T>(args: EventArgs<T>): Promise<void> { }
async beforeUpdate<T>(args: EventArgs<T>): Promise<void> { }
async afterUpdate<T>(args: EventArgs<T>): Promise<void> { }
async beforeUpsert<T>(args: EventArgs<T>): Promise<void> { }
async afterUpsert<T>(args: EventArgs<T>): Promise<void> { }
async beforeDelete<T>(args: EventArgs<T>): Promise<void> { }
async afterDelete<T>(args: EventArgs<T>): Promise<void> { }
// Flush events
async beforeFlush(args: FlushEventArgs): Promise<void> { }
async onFlush(args: FlushEventArgs): Promise<void> { }
async afterFlush(args: FlushEventArgs): Promise<void> { }
// Transaction events
async beforeTransactionStart(args: TransactionEventArgs): Promise<void> { }
async afterTransactionStart(args: TransactionEventArgs): Promise<void> { }
async beforeTransactionCommit(args: TransactionEventArgs): Promise<void> { }
async afterTransactionCommit(args: TransactionEventArgs): Promise<void> { }
async beforeTransactionRollback(args: TransactionEventArgs): Promise<void> { }
async afterTransactionRollback(args: TransactionEventArgs): Promise<void> { }
}
Event handlers receive an EventArgs object:
interface EventArgs<T> {
entity: T;
em: EntityManager;
changeSet?: ChangeSet<T>; // Available during flush operations
}
interface ChangeSet<T> {
name: string; // Entity name
collection: string; // Database table name
type: ChangeSetType; // 'create' | 'update' | 'delete' | 'delete_early'
entity: T; // The entity instance
payload: EntityData<T>; // Changes for the UPDATE query
persisted: boolean; // Whether already executed
originalEntity?: EntityData<T>; // Snapshot when loaded from database
}
Flush events fire during em.flush() and are not tied to any specific entity:
| Event | When it fires | Use case |
|---|---|---|
beforeFlush | Before change sets are computed | Safe to persist new entities here |
onFlush | After change sets are computed | Modify or add change sets |
afterFlush | After all queries complete | Cleanup, notifications |
interface FlushEventArgs extends Omit<EventArgs<unknown>, 'entity'> {
uow: UnitOfWork;
}
getSubscribedEntities()has no effect on flush events - they always fire regardless of entity type filters.
The UnitOfWork provides methods to inspect pending changes:
async onFlush(args: FlushEventArgs) {
const uow = args.uow;
// All pending change sets
const changeSets = uow.getChangeSets();
// Original data when entity was loaded
const original = uow.getOriginalEntityData(entity);
// Entities marked for persist/remove
const toInsert = uow.getPersistStack();
const toDelete = uow.getRemoveStack();
// Collection modifications
const collectionUpdates = uow.getCollectionUpdates();
}
Use beforeFlush to safely create new entities:
async beforeFlush(args: FlushEventArgs) {
// Safe to create and persist new entities here
const log = args.em.create(AuditLog, { action: 'flush', timestamp: new Date() });
}
In onFlush, you can add or modify change sets:
async onFlush(args: FlushEventArgs) {
const changeSets = args.uow.getChangeSets();
const cs = changeSets.find(cs =>
cs.type === ChangeSetType.CREATE && cs.name === 'FooBar'
);
if (cs) {
// Create a related entity
const related = new FooBaz();
related.name = 'auto-created';
cs.entity.baz = related;
// Compute change set for the new entity
args.uow.computeChangeSet(related);
// Recompute the original entity's change set
args.uow.recomputeSingleChangeSet(cs.entity);
}
}
To convert a delete to an update (e.g. for soft deletes):
async onFlush(args: FlushEventArgs) {
for (const cs of args.uow.getChangeSets()) {
if (cs.type !== ChangeSetType.DELETE) {
continue;
}
if (!cs.meta.properties.deletedAt) {
continue;
}
cs.entity.deletedAt = new Date();
args.uow.computeChangeSet(cs.entity, ChangeSetType.UPDATE);
}
}
To convert an update to a delete:
async onFlush(args: FlushEventArgs) {
const cs = args.uow.getChangeSets().find(cs =>
cs.type === ChangeSetType.UPDATE && cs.entity.shouldDelete
);
if (cs) {
args.uow.computeChangeSet(cs.entity, ChangeSetType.DELETE);
}
}
Transaction events fire at transaction boundaries:
| Event | When it fires |
|---|---|
beforeTransactionStart | Before a transaction begins |
afterTransactionStart | After a transaction begins |
beforeTransactionCommit | Before a transaction commits |
afterTransactionCommit | After a transaction commits |
beforeTransactionRollback | Before a transaction rolls back |
afterTransactionRollback | After a transaction rolls back |
interface TransactionEventArgs extends Omit<EventArgs<unknown>, 'entity' | 'changeSet'> {
transaction?: Transaction; // Native transaction (e.g., Knex client for SQL)
uow?: UnitOfWork;
}
Transaction events are entity-agnostic - getSubscribedEntities() has no effect on them.