Skip to content

Commit f032dd7

Browse files
authored
Feature/storm 99 (#100)
Add transaction callbacks. (#99)
1 parent a8d755e commit f032dd7

File tree

6 files changed

+1086
-30
lines changed

6 files changed

+1086
-30
lines changed

docs/comparison.md

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ JPA (typically implemented by Hibernate) is the most widely used persistence fra
8484
### When to Choose Storm
8585

8686
- You want predictable, explicit database behavior
87+
- You want concise entity definitions with minimal boilerplate
8788
- N+1 queries have been a recurring problem
8889
- You prefer immutable data structures
8990
- You value simplicity over complexity
@@ -114,7 +115,7 @@ Spring Data JPA wraps JPA with a repository abstraction that derives query imple
114115

115116
### When to Choose Storm
116117

117-
- You want stateless, immutable entities
118+
- You want stateless, immutable entities with minimal boilerplate
118119
- You prefer explicit query logic over naming conventions
119120
- You want to avoid JPA's complexity
120121
- You want a lightweight, minimal dependency footprint
@@ -145,7 +146,7 @@ MyBatis is a SQL mapper that gives you full control over every query. You write
145146

146147
### When to Choose Storm
147148

148-
- You want automatic entity mapping without XML
149+
- You want automatic entity mapping without XML and minimal boilerplate
149150
- You prefer type-safe queries over string SQL
150151
- You want relationships handled automatically
151152
- You value compile-time safety
@@ -204,7 +205,7 @@ JDBI is a lightweight SQL convenience library that sits just above JDBC. It hand
204205

205206
### When to Choose Storm
206207

207-
- You want automatic entity mapping
208+
- You want automatic entity mapping with concise entity definitions
208209
- You need relationship handling
209210
- You prefer type-safe queries over raw SQL
210211

@@ -257,10 +258,68 @@ Storm supports all seven standard propagation modes natively in its `transaction
257258

258259
This means Storm's programmatic API can express patterns like audit logging (`REQUIRES_NEW`), defensive boundary enforcement (`MANDATORY`, `NEVER`), and non-transactional operations (`NOT_SUPPORTED`) directly in code, while Exposed requires Spring integration for these use cases. See [Transactions](transactions.md) for details and examples of each propagation mode.
259260

261+
#### Transaction Callbacks
262+
263+
Both frameworks allow running logic after a transaction commits or rolls back, but the APIs differ significantly.
264+
265+
Storm provides `onCommit` and `onRollback` callbacks on the `Transaction` object. Callbacks accept suspend functions, execute in registration order, and are resilient to individual failures (remaining callbacks still run). When a callback is registered inside a joined scope (`REQUIRED`, `NESTED`), it is automatically deferred to the outermost physical transaction's commit or rollback, so it only fires when data is actually durable:
266+
267+
```kotlin
268+
transaction {
269+
orderRepository.insert(order)
270+
onCommit { emailService.sendConfirmation(order) } // Fires after physical commit
271+
}
272+
```
273+
274+
Exposed uses a `StatementInterceptor` interface with lifecycle methods (`beforeCommit`, `afterCommit`, `beforeRollback`, `afterRollback`, among others) that is registered on the transaction via `registerInterceptor()`. Global interceptors can be registered via Java `ServiceLoader`. This approach is well suited for cross-cutting concerns that apply to many transactions:
275+
276+
```kotlin
277+
transaction {
278+
// Exposed: register an interceptor
279+
registerInterceptor(object : StatementInterceptor {
280+
override fun afterCommit(transaction: Transaction) {
281+
emailService.sendConfirmation(order)
282+
}
283+
})
284+
OrderTable.insert { it[id] = order.id }
285+
}
286+
```
287+
288+
| Aspect | Storm | Exposed |
289+
|--------|-------|---------|
290+
| API style | Lambda (`onCommit { }`) | Interface (`StatementInterceptor`) |
291+
| Suspend support | Yes (JDBC) | R2DBC only (`SuspendStatementInterceptor`) |
292+
| Nested transaction behavior | Deferred to physical commit | Fires after savepoint release (data not yet durable) |
293+
| Callback isolation | Yes (remaining callbacks still run on failure) | No (exception propagates, skipping remaining interceptors) |
294+
| Global interceptors | No | Yes (via `ServiceLoader`) |
295+
| Additional hooks | No | `beforeCommit`, `beforeRollback`, `beforeExecution`, `afterExecution` |
296+
297+
The most significant behavioral difference is with nested transactions. Exposed's `afterCommit` fires on the nested transaction's own "commit," which for savepoint-based nesting is just a savepoint release, not an actual database commit. If the outer transaction subsequently rolls back, the `afterCommit` callback will have already executed despite the data never becoming durable. Storm avoids this by deferring callbacks to the outermost physical transaction.
298+
299+
Storm's callback isolation behavior (remaining callbacks still execute when one fails) follows the same approach as Spring's `TransactionSynchronization`, where post-commit and post-completion callbacks are invoked independently. Since callbacks fire after the transaction outcome is final, there is nothing to undo; silently skipping remaining side effects because of one failure would be worse than running them all and surfacing the first exception.
300+
301+
Exposed's `StatementInterceptor` also provides hooks that Storm intentionally does not offer: `beforeCommit`, `beforeRollback`, and statement-level interceptors (`beforeExecution`, `afterExecution`). In Storm's stateless model, pre-commit logic belongs at the end of the `transaction { }` block itself, since there is no persistence context to flush or dirty state to reconcile before the commit. Statement-level observability is covered by Storm's [`@SqlLog`](sql-logging.md) annotation and `SqlCapture` test utility rather than a general interceptor mechanism.
302+
303+
#### Schema Migration
304+
305+
Exposed provides built-in schema management through its `SchemaUtils` utility. You can create tables, add missing columns, and generate migration statements programmatically:
306+
307+
```kotlin
308+
transaction {
309+
SchemaUtils.create(UsersTable, OrdersTable) // CREATE TABLE IF NOT EXISTS
310+
SchemaUtils.createMissingTablesAndColumns(UsersTable) // ALTER TABLE ADD COLUMN ...
311+
SchemaUtils.statementsRequiredToActualizeScheme() // Returns DDL statements without executing
312+
}
313+
```
314+
315+
This is convenient for prototyping and simple applications. For production use, JetBrains recommends pairing Exposed with a dedicated migration tool like Flyway or Liquibase, since `SchemaUtils` does not handle column removal, type changes, or data migration.
316+
317+
Storm does not include schema management or migration utilities. Schema management is expected to be handled externally using tools like Flyway, Liquibase, or plain SQL scripts. Storm's [schema validation](validation.md) feature can verify at startup that entity definitions match the database schema, catching mismatches early without modifying the schema itself.
318+
260319
### When to Choose Storm
261320

262321
- You need Kotlin and Java support
263-
- You want immutable entities without base class inheritance
322+
- You want concise, immutable entities without base class inheritance
264323
- You prefer annotation-based entity definitions
265324
- N+1 queries are a concern
266325
- You want relationships loaded automatically
@@ -298,7 +357,7 @@ Ktorm is a lightweight Kotlin ORM that uses entity interfaces and DSL-based tabl
298357
### When to Choose Storm
299358

300359
- You need Kotlin and Java support
301-
- You want immutable data classes, not interfaces
360+
- You want concise, immutable data classes instead of mutable interfaces
302361
- You prefer annotation-based definitions
303362
- N+1 prevention is important
304363
- You want automatic relationship loading

0 commit comments

Comments
 (0)