TxManager is AutoCloseable and releases what it was built on: JdbcTxManager its data source when closeable, HibernateTxManager its session factory and then the data source Hibernate was handed, which Hibernate itself never closes. DataExtension registers the close as an onClose callback, so a stopped app no longer leaves its pool connected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.3 KiB
flash-ext-data-core
Shared core for Flash's data layer.
Purpose
This module defines the transactional contract shared across backend implementations. It does not talk to Hibernate or JDBC directly: it exposes abstractions and a minimal runtime, nothing else.
Components
TxDefinition: immutable transaction metadata.TxStatus: runtime state returned by the manager.TxManager: thebegin/commit/rollbackcontract.Tx: runtime orchestration and the per-thread transaction stack.ResourceRegistry: thread-local storage for resources and synchronizations.Repository<T, ID>: self-transactional base repository.Spec<T>: composable predicate.Query<T>: query object carrying spec, sort and paging.SpecBuilder<T>: fluent DSL for building typed specs.RepositorySupport<T, ID>: shared internal helper.TransactionPropagation: propagation semantics.TransactionIsolation: isolation level.TxSynchronization: lifecycle hooks (see below).
Execution model
The flow is:
Tx.call(definition, work)callsTxManager.begin(definition).- The
TxManagercreates a backend-specificTxStatus. - The status is pushed onto the thread-local stack.
- The work uses
Tx.resource(Class)to obtain the current resource. - When the work ends,
Txchooses betweencommitandrollback. - The stack is popped, and the thread-local is cleared once it is empty.
Supported propagation
REQUIRED: use the active transaction, or open a new one.REQUIRES_NEW: suspend the current transaction and open a new one.SUPPORTS: join the active transaction if there is one, otherwise run without a transaction.NOT_SUPPORTED: suspend the current transaction and run without one.MANDATORY: require an active transaction.
Synchronizations (TxSynchronization)
Lifecycle hooks for one transaction, registered through Data.afterCommit(...) (or directly
with ResourceRegistry.addSynchronization(...)).
Every callback belongs to exactly the innermost transaction active at registration time, and fires exactly once, when that transaction completes:
- a joined inner transaction (
REQUIRED) is not a transaction of its own, so callbacks registered inside one wait for the outermost commit; - a
REQUIRES_NEWtransaction is, so completing it fires only its own callbacks and leaves the suspended outer transaction's pending.
Which side of the commit each hook sits on
| hook | when | resource |
|---|---|---|
beforeCommit(readOnly) |
immediately before the real commit | session/connection still bound, transaction still active |
afterCommit() / afterRollback() |
after completion | resource already unbound |
afterCompletion(outcome) |
after the two above | resource already unbound |
beforeCommit is the only hook that can still write through the same resource and have the write
land in the same atomic unit: flush a buffer, stamp an audit row, materialize a derived value. It
is skipped when the transaction is already rollback-only, since there is no commit to precede.
Post-completion callbacks run with the resource unbound instead: one that opens its own transaction gets a fresh one rather than joining the transaction that just finished. That is what makes them the right place to refresh a cache, enqueue a message, or notify anything outside the database.
Failure
Throwing from beforeCommit vetoes the commit: the transaction is rolled back,
afterRollback/afterCompletion(ROLLED_BACK) fire, and the exception reaches the caller. That is
the reason the hook runs before the commit rather than after — it can still refuse.
The post-completion hooks have no such power: the transaction is already over by the time they run, so an exception propagates but changes nothing already committed, and stops the callbacks queued behind it.
Using Repository
Repository is the shared base for concrete repositories. Every public operation internally uses a
REQUIRED transaction, read-only where applicable.
Subclasses implement the doXxx(...) methods:
doFind(Query<T>)doFindOne(Spec<T>)doFindPage(Query<T>)doDeleteAll(Spec<T>)doUpdateAll(Spec<T>, T)
The old findAll(...) and findPage(...) overloads were reduced to a combination of Query<T> and
Spec<T>.
public abstract class Repository<T, ID> {
protected Repository(Tx tx) { ... }
protected final <R> R tx(Tx.TxCallable<R> work) { ... }
}
Composing with Flash
DataExtension registers:
Txin theFlashContextTxManagerin theFlashContext- an annotation processor for
@Transactional TxManager.close()as anonClosecallback, so stopping the app releases the manager's session factory and connection pool; give the manager a pool you want closed with the app
This makes the data layer composable with Flash's extension system without global state.
Implementation notes
- The transaction stack is thread-local and is cleared once it becomes empty.
- Backend resources are suspended and restored for
REQUIRES_NEWandNOT_SUPPORTED. TxSynchronizationis the hook point for commit/rollback/completion callbacks.- Synchronizations live in a thread-local list; every new transaction records how many were already
registered when it opened and fires only its own tail, so a
REQUIRES_NEWdoes not drag along the suspended transaction's callbacks.