ORM - Events
ORM - Events
Hibernate fires events at key points in an entity's lifecycle — before insert, after load, on flush, etc. You can hook into these events to implement audit logging, auto-set timestamps, enforce business rules, or sync data.
Enabling Events
Entity-level events (methods defined directly in entity CFCs) fire automatically — no configuration needed.
The global event handler requires eventHandling: true in your ORM - Configuration:
this.ormSettings = {
eventHandling: true,
eventHandler: "models.GlobalEventHandler"
};
Without eventHandling: true, the global handler CFC is not registered. Entity-level events are unaffected.
Entity Events
Define event methods directly in your entity CFC. These fire only for that entity:
component persistent="true" table="users" accessors="true" {
property name="id" fieldtype="id" ormtype="string";
property name="name" ormtype="string";
property name="username" ormtype="string" notnull="true";
property name="password" ormtype="string" notnull="true";
property name="dateCreated" ormtype="timestamp";
property name="dateUpdated" ormtype="timestamp";
function preInsert() {
setDateCreated( now() );
if ( isNull( getPassword() ) )
setPassword( createUUID() );
}
function preUpdate() {
setDateUpdated( now() );
}
}
Available Entity Events
| Event | Fires When | Arguments |
|---|---|---|
preInsert() |
Before INSERT SQL executes | none |
postInsert() |
After INSERT SQL executes | none |
preUpdate() |
Before UPDATE SQL executes | none |
postUpdate() |
After UPDATE SQL executes | none |
preDelete() |
Before DELETE SQL executes | none |
postDelete() |
After DELETE SQL executes | none |
preLoad() |
Before entity is populated from DB | none |
postLoad() |
After entity is populated from DB | none |
Global Event Handler
A global event handler receives events for ALL entities. Set it in your ormSettings:
this.ormSettings = {
eventHandling: true,
eventHandler: "models.GlobalEventHandler"
};
The handler CFC should NOT be persistent:
// models/GlobalEventHandler.cfc
component persistent="false" {
function preInsert( entity ) {
// runs for every entity insert
}
function postInsert( entity ) {
}
function preUpdate( entity, struct oldData ) {
// oldData contains the previous values
}
function postUpdate( entity ) {
}
function preDelete( entity ) {
}
function onDelete( entity ) {
}
function postDelete( entity ) {
}
function preLoad( entity ) {
}
function postLoad( entity ) {
}
function onFlush( entity ) {
}
function onClear( entity ) {
}
function onEvict() {
}
function onAutoFlush() {
}
function onDirtyCheck() {
}
}
All Global Events
| Event | Fires When | Arguments |
|---|---|---|
preInsert( entity ) |
Before INSERT | The entity being inserted |
postInsert( entity ) |
After INSERT | The entity that was inserted |
preUpdate( entity, oldData ) |
Before UPDATE | The entity and a struct of previous values |
postUpdate( entity ) |
After UPDATE | The entity that was updated |
preDelete( entity ) |
Before DELETE | The entity being deleted |
onDelete( entity ) |
During DELETE processing | The entity being deleted |
postDelete( entity ) |
After DELETE | The entity that was deleted |
preLoad( entity ) |
Before population from DB | The empty entity |
postLoad( entity ) |
After population from DB | The loaded entity |
onFlush( entity ) |
When session flush begins | The entity being flushed |
onClear( entity ) |
When session is cleared | — |
onEvict() |
When an entity is evicted from the session | — |
onAutoFlush() |
When an auto-flush is triggered | — |
onDirtyCheck() |
When Hibernate checks for dirty entities | — |
Event Firing Order
When both entity-level and global event handlers define the same event, the entity event fires first, then the global handler. (Changed in 5.6 — previously the global handler fired first, LDEV-4561)
For an insert with both handlers:
- Entity
preInsert() - Global
preInsert( entity ) - INSERT SQL executes
- Entity
postInsert() - Global
postInsert( entity )
Events Fire on Flush, Not on Save
This is a critical distinction. Calling entitySave() does NOT trigger preInsert — the event fires when the session flushes:
entity = entityNew( "Auto", { id: createUUID(), make: "BMW" } );
entitySave( entity );
// preInsert has NOT fired yet
ormFlush();
// NOW preInsert fires, then INSERT executes, then postInsert fires
This means:
- Inside a transaction, events fire when the transaction commits (which triggers a flush)
- With
flushAtRequestEnd: true, events fire at the end of the request - With explicit
ormFlush(), events fire immediately
Setting Values in preInsert / preUpdate
A common pattern: auto-set timestamps, generate default values, or enforce constraints in preInsert or preUpdate. Hibernate syncs changes made in these handlers back to the database state automatically:
function preInsert() {
setDateCreated( now() );
if ( isNull( getPassword() ) )
setPassword( createUUID() );
}
This works because the extension's EventListenerIntegrator calls persistEntityChangesToState() after your handler runs — syncing CFC property mutations back to the Hibernate state array that gets persisted.
This is a 5.6 improvement. In earlier versions, setting a NOT NULL property in
preInsertcould throw a constraint violation because Hibernate's nullability check ran before the handler had a chance to set the value. In 5.6, the nullability check runs AFTER event handlers.
Practical Patterns
Audit Logging
// GlobalEventHandler.cfc
component persistent="false" {
function preInsert( entity ) {
logEvent( "INSERT", entity );
}
function preUpdate( entity, struct oldData ) {
logEvent( "UPDATE", entity );
}
function preDelete( entity ) {
logEvent( "DELETE", entity );
}
private function logEvent( action, entity ) {
cflog(
text: "#action# #getMetadata( entity ).getName()#",
log: "orm"
);
}
}
Auto-Timestamps
component persistent="true" table="articles" accessors="true" {
property name="id" fieldtype="id" ormtype="string";
property name="title" ormtype="string";
property name="createdAt" ormtype="timestamp";
property name="updatedAt" ormtype="timestamp";
function preInsert() {
setCreatedAt( now() );
setUpdatedAt( now() );
}
function preUpdate() {
setUpdatedAt( now() );
}
}
Conditional Logic in Global Handler
Use getMetadata() to branch on entity type:
function preInsert( entity ) {
var entityName = getMetadata( entity ).getName();
if ( entityName == "User" ) {
// user-specific logic
}
}
Common Mistakes
Don't Call ORMFlush() in Event Handlers
Calling ormFlush() inside preInsert, preUpdate, or any event handler triggers another flush, which fires more events, which call ormFlush() again — infinite loop:
// BAD — infinite loop
function preInsert() {
setCreatedAt( now() );
ormFlush(); // DON'T DO THIS
}
Just set values and return. The flush that triggered the event will persist your changes.
Don't Call entitySave() in Event Handlers
Similarly, calling entitySave() on a different entity inside an event handler can cause unexpected cascading flushes. If you need to create related entities, do it outside the event handler.
Events Don't Fire for Bulk DML
HQL UPDATE and DELETE statements bypass the entity lifecycle entirely — no events fire:
// This does NOT trigger preDelete on any entity
ORMExecuteQuery( "DELETE FROM Product WHERE active = false" );
If you need events to fire, load and delete entities individually.
What's Next?
- ORM - Sessions and Transactions — how flush timing affects when events fire
- ORM - Configuration —
eventHandlingandeventHandlersettings - ORM - Troubleshooting — StackOverflowError from ormFlush() in handlers