Tentackle Persistence — The PDO-Aware Persistence Layer¶
Overview and Motivation¶
The tentackle-persistence module is where the two halves of Tentackle's architecture finally meet: the
Persistent Domain Object (PDO) abstraction from tentackle-pdo
and the raw, dialect-aware database machinery from tentackle-database. It
provides the concrete persistence implementation behind every PDO — the code that actually selects, inserts,
updates and deletes domain objects, manages their caches, locks, number ranges, security rules and the remoting
that lets all of this work transparently across JVMs.
In the PDO pattern (see pdo.md) a domain object is a dynamic proxy that combines two injected implementations:
- a domain implementation (business logic), and
- a persistence implementation (database logic).
This module supplies the base classes for the persistence half:
AbstractPersistentObject for entities and
AbstractPersistentOperation for operations.
The application's generated persistence classes extend these, the Wurbelizer fills in the entity-specific mapping
code (see persistence-wurblets),
and at runtime the PDO proxy delegates all persistence calls here.
Where tentackle-database knows about tables, columns, PreparedStatements, and
result sets (its AbstractDbObject/AbstractDbOperation), this module raises that to the level of domain
objects: it understands aggregates and their components, eager joins, the modification tracker, token locks and
the domain context. It is therefore the topmost runtime layer of the persistence stack — everything above it
(domain logic, UI) deals only in PDOs.
Design principles¶
- Bridge, not a new model. The module does not invent its own entity model; it implements the
PersistentObject/PersistentOperationcontracts fromtentackle-pdoon top of theAbstractDbObject/AbstractDbOperationprimitives fromtentackle-database. - Generated, not handwritten, mappings. The per-entity SQL and column mapping live in generated code woven into the application's persistence classes by the Wurbelizer; the handwritten code here is the generic engine those classes plug into.
- Aggregate-aware. Roots and their components are first-class: operations like
insertPlainWithComponents/deletePlainWithComponentsand eagerJoins understand composite relations so DDD invariants are preserved. - Location transparency via TRIP. Every persistence capability has a remote delegate counterpart in the
*.trippackages, so the identical PDO code runs whether the database is local or reached through a Tentackle server, which in turn may itself be a client of another Tentackle server, and so on. - SPI-replaced infrastructure. Several singletons (
DbUtilities,ModificationTracker,DbClassVariablesFactory, the remote-session factory and the remote-delegate locator) are replaced with PDO-aware implementations through Tentackle's@Service/ServiceFindermechanism, so the lower layers transparently become PDO-aware.
Key Concepts¶
AbstractPersistentObject¶
AbstractPersistentObject<T,P> is the heart of the
module — the base persistence implementation of an entity PDO. It extends
AbstractDbObject<P> (database layer) and implements PersistentObject<T> (PDO contract), where T is the PDO
interface and P the concrete persistence class. It is @TripReferencable(NEVER) because only the PDO proxy
can be referenced when it travels across the wire, never the implementation.
Its (large) API covers the full persistence lifecycle of a domain object:
| Group | Examples | Purpose |
|---|---|---|
| Single-object CRUD | select(id), selectForUpdate, selectTokenLocked, persist, persistTokenLocked, save, delete, reload |
Read and write one entity, optionally locking it. |
| Bulk / aggregate | insertPlainWithComponents, deletePlainWithComponents, selectAll, selectAny(ids), save(Collection,…) |
Operate on collections and on roots together with their components. |
| Querying | selectByTemplate, selectByNormText, findDuplicate, selectLatest, selectAllAsCursor, selectByNormTextAsCursor |
Higher-level finders, including streaming via cursors. |
| Caching | selectCached, selectCachedOnly, selectAllCached, selectAnyCached, selectForCache, expireCache |
The PDO cache, kept coherent via the table serials. |
| Modification / serials | countModification, selectAllWithExpiredTableSerials, updateRootContext |
Hook into change tracking and table serials. |
| Token locking | the editedBy / editedSince / editedExpiry token-lock attributes plus the lock-aware select*/reload* variants |
Cooperative, persisted "currently edited by user X" locks (see locking.md). |
| Normtext | updateNormText, selectByNormText |
Phonetically normalized search text (see NormText). |
An entity optionally also carries a normText column (a phonetically normalized search string, model option
normtext) and the token-lock columns (model option tokenlock), which is why those concerns are baked into the
base class. The low-level insertObject/updateObject/deleteObject are not part of the PDO API and cannot be
invoked remotely; applications use persist, save and delete on root entities. Inserts, updates and deletes
take part in JDBC batching if the transaction runs in
batched mode.
AbstractPersistentOperation¶
AbstractPersistentOperation<T,P> is the
persistence base for operations — units of work that are not a single entity but still need a persistence
context, typically complex multistep transactions (see
operation.md). It extends AbstractDbOperation<P> and
implements PersistentOperation<T> / PersistenceDelegate<T>, is bound to a DomainContext, and is remoting
capable like entities.
Class variables¶
Tentackle keeps per-class (static) metadata in class-variable objects.
PersistentObjectClassVariables and
PersistentOperationClassVariables extend
the database layer's DbObjectClassVariables / DbOperationClassVariables with PDO-level information. They are
produced by PersistentClassVariablesFactory,
which is registered as a @Service(DbClassVariablesFactory.class) so it replaces and extends the database
layer's factory — making all class variables PDO-aware. The CLASSVARIABLES of a persistence class, including
its eager joins, are generated by the ClassVariables wurblet (see
persistence-wurblets).
Modification tracking¶
PdoModificationTracker is the persistence-layer
implementation of the ModificationTracker from tentackle-session
(registered via @Service(ModificationTracker.class)). It maintains the table serials that drive cache expiry
and cross-client change notification: when an entity is written, the modification serial of its table advances;
caches and remote clients compare serials to learn what to refresh. It differs from the database layer's tracker
only in mapping PDO interfaces to their table names. See
modification-tracking.md for the
full picture.
Token locking¶
A token lock is a cooperative, persisted lock expressing "this object is currently being edited by user X until time Y". Unlike a database row lock, it survives across transactions and is visible to other users.
The token lock is opt-in: an entity gets the editedBy/editedSince/editedExpiry columns by declaring the
tokenlock option in its model.
LockManager— tracks the state of all token locks in parallel to the PDO columns, so the full set of locks is known without scanning every table. It exists to clean up locks when a client logs out or its session crashes (drop that user's locks) or when the server restarts (drop all locks). It is only enabled on servers connected directly to the database; in 2-tier and remote-client setups it is disabled.TokenLock— a lock as theLockManagerknows it: the PDO's class ID and ID, the holder, and since/expiry.DefaultLockManager(@Service(LockManager.class)) persists locks in a dedicated table ofDbTokenLockrows. Applications can keep selected locks across a session cleanup or server restart by overridingisTokenLockToKeep.
See locking.md for how token locks and the optimistic serial column work together.
Joins¶
Join and JoinedSelect
implement PDO-level joins — fetching two related PDOs in a single SELECT. They back both eager joins
(relations marked to load with their root) and explicit domain-driven joins, and work for composite
(aggregate component) and non-composite relations alike.
A Join is a description of how to join that is not modified once set up. Model-based eager joins are created
once in the static class variables of a persistence class, so the same Join instances are used concurrently by
every thread selecting that entity. The bookkeeping of which PDOs have already been joined therefore lives in
JoinState, which is created per query and owned by the
JoinedSelect — never in the Join itself.
See eager-relations.md for how
eager joins are declared in the model, and
persistence-wurblet-api.md
for using JoinedSelect in handwritten finders.
Cursors¶
ResultSetCursor is a ScrollableResource<T> that turns a
ResultSetWrapper into a forward/scrollable stream of PDOs, so large result sets can be processed without
materializing them all in memory. Its remote counterpart is
RemoteResultSetCursor /
RemoteResultSetCursorImpl, so cursors work
over a remote connection too.
See cursors.md for the full picture: how one class serves both session types, block fetching, the join constraint, and cursor lifetime.
NormText¶
NormText builds a normtext — a phonetically normalized string —
from user input, for searching PDO normtext columns with LIKE/NOT LIKE. Entities declaring the normtext
model option maintain such a column, built from the attributes and relations flagged for it, enabling tolerant,
accent/case-insensitive searches via selectByNormText and selectByNormTextAsCursor.
The search string is normalized the same way as the column and wrapped in %…%, so it matches anywhere.
A leading ! or - inverts the search (NOT LIKE), and an empty string matches all.
Utilities¶
PersistenceUtilities is registered as
@Service(DbUtilities.class) and replaces the database layer's DbUtilities with a PDO-aware version — the
single most visible example of how this module quietly upgrades the layer beneath it. Among others, it creates
and selects PDOs where the database layer expects low-level objects, forwards commits and rollbacks to the
LockManager, releases the token locks of a closed client session, and collects the caches to preload.
Remoting (TRIP)¶
Persistence is fully location transparent: the same PDO API runs against a local database or a remote
Tentackle server. This is achieved by pairing each persistence capability with a TRIP remote delegate — an
interface (the contract) plus an …Impl (the server-side executor):
| Delegate (interface / impl) | Mirrors |
|---|---|
AbstractPersistentObjectRemoteDelegate / …Impl |
AbstractPersistentObject (entity CRUD/queries) |
AbstractPersistentOperationRemoteDelegate / …Impl |
AbstractPersistentOperation |
AdminExtensionAdapterRemoteDelegate / …Impl |
admin/session-management operations |
RemoteResultSetCursor / …Impl |
streaming cursors |
PdoCacheOperationRemoteDelegate / …Impl |
cache preloading (PdoCacheOperation) |
PdoModificationTrackerRemoteDelegate / …Impl |
PdoModificationTracker |
The low-level methods inherited from the database layer (insertObject, updatePlain, selectAllObjects, …)
are disabled in AbstractPersistentObjectRemoteDelegateImpl, so a malicious client cannot bypass the PDO-level
security checks.
PdoComponentTripFilter is a TripFilter that
serializes PDOs without their related objects (except embedded ones), so they are loaded lazily on the receiving
side — see Filtering components with TripFilter.
TripModificationLogSerializer uses that
filter to store the state of modified PDOs in the modification log, with a TRIP dictionary that spans several
modlogs and is reset by count or time. The dictionary is commit-gated and split into epochs, so the modlog-writing
transactions may overlap — see
Modification logging.
PdoRemoteDelegateLocator
(@Service(RemoteDbDelegateLocator.class)) locates the right delegate for a PDO. On the client, a PDO method call
is routed to the matching delegate; on the server, the …Impl runs the real AbstractPersistentObject logic
against a local database connection and ships the results back via TRIP.
Session-side remoting is provided by
PdoRemoteSessionAdapter (a PDO-aware
RemoteSessionAdapter, which also hands out session extensions after checking the execute permission) and
PdoRemoteSessionFactory
(@Service(RemoteSessionFactory.class)), while
DefaultPersistenceDelegateLinker
(@Service(PersistenceDelegateLinker.class)) wires a PDO proxy to its persistence delegate, and
AdminExtensionAdapter
(@Service(AdminExtension.class)) adds administrative session services: listing, logging out and
killing client sessions (see Session administration).
Built-in Persistence Implementations¶
The module ships ready-made persistence implementations for the framework's own infrastructure PDOs. Each is
annotated @PersistentObjectService(...) so it is discovered as the persistence half of the corresponding PDO:
- Number sources (
org.tentackle.persist.ns) — allocation of business numbers (invoice numbers, customer numbers, …) from number pools and their ranges, not of technical IDs:NumberPoolPersistenceImplandNumberRangePersistenceImpl, with their remote delegates underns/trip. See ns.md. - Security rules (
org.tentackle.persist.security) —SecurityPersistenceImplpersists the access-control rules, with its remote delegate undersecurity/trip. See security.md.
Besides, PdoCacheOperationPersistenceImpl
implements the PdoCacheOperation that preloads the caches of all persistence classes annotated with
@PreloadCache — for remote clients in a single round trip.
See Preloading Caches at Startup.
Server Application Lifecycle¶
The org.tentackle.persist.app package provides the entry points for the middle-tier server:
AbstractServerApplication— base class handling server startup/shutdown, configuration and the persistence environment. A server connected directly to the database serves its clients from a session pool with its own connection manager; a server that is itself a client of another server uses a pool of remote sessions instead. At startup, all validators are validated, so configuration errors show up immediately.ServerApplication— the abstract base of TRIP application servers clients connect to; it starts theTripServerhosting the remote delegates described above and, if connected directly to the database, initializes theLockManager.
See application.md for the application lifecycle and tiers.md for how servers cascade.
Package Layout¶
| Package | Contents |
|---|---|
org.tentackle.persist |
The core: AbstractPersistentObject, AbstractPersistentOperation, class-variables and their factory, the modification tracker, token-lock interfaces, joins (Join, JoinedSelect, JoinState), cursors, normtext, cache preloading (PreloadCache, PdoCacheOperationPersistenceImpl), the PDO-aware utilities and the session/delegate adapters. |
org.tentackle.persist.app |
Server-application lifecycle (AbstractServerApplication, ServerApplication). |
org.tentackle.persist.lock |
The default LockManager and its persisted DbTokenLock. |
org.tentackle.persist.ns (+ ns.trip) |
Persistence for number sources (pools/ranges) and their remoting. |
org.tentackle.persist.security (+ security.trip) |
Persistence for security rules and their remoting. |
org.tentackle.persist.trip |
The PDO-aware TRIP remote delegates (entity, operation, admin, cursor, cache preloading, modification tracker), the delegate locator and the PdoComponentTripFilter. |
org.tentackle.persist.service |
The Hook (ModuleHook) for i18n bundle resolution. |
How It Fits Together¶
A typical read of an entity through a PDO proxy flows like this:
PDO proxy (Customer)
│ select(id)
▼
persistence delegate ── AbstractPersistentObject.select(id)
│
local ──────────────┐ remote ─────────────┐
▼ │ ▼ │
AbstractDbObject │ builds SQL via PdoRemoteDelegateLocator
+ Backend (SQL) │ the Backend → …RemoteDelegate (TRIP)
▼ │ ▼ (on server)
JDBC ResultSet ─────┘ …RemoteDelegateImpl runs the
│ *same* AbstractPersistentObject
▼ logic against a local connection
mapped PDO(s) returned
For a local session, AbstractPersistentObject assembles SQL via the
Backend and executes it through the
tentackle-database primitives. For a remote session, the call is shipped to
the server, where the corresponding …RemoteDelegateImpl runs the very same logic against its local connection and
returns the PDOs over TRIP. Caches stay coherent because every write advances the table serial tracked by the
PdoModificationTracker, which clients consult to expire stale entries.
The per-entity SQL, column mapping, finders, and relations are generated into the application's persistence classes by the Wurbelizer using the model; this module is the generic runtime those generated classes extend. See persistence-wurblets for the generation side and model-definition for where entities, relations, and attributes are declared.
Module Dependencies¶
tentackle-persistence sits at the top of the persistence stack:
- It requires transitive
tentackle-pdo(the PDO contracts it implements) andtentackle-database(the low-level DB engine it builds on), plus (non-transitively)java.namingto detect a container. - It opens its
persist,persist.lock,persist.ns,persist.securityandpersist.trippackages toorg.tentackle.corefor TRIP serialization. - It provides a
ModuleHookservice (org.tentackle.persist.service.Hook) for i18n. - Through
@Service/@MappedServiceit replaces several lower-layer singletons with PDO-aware versions (DbUtilities→PersistenceUtilities,ModificationTracker→PdoModificationTracker,DbClassVariablesFactory→PersistentClassVariablesFactory,RemoteSessionFactory→PdoRemoteSessionFactory,RemoteDbDelegateLocator→PdoRemoteDelegateLocator), so code written against the database layer automatically becomes PDO-aware at runtime.
Related Documentation¶
- Locking — token locks and the optimistic
serialcolumn. - Cursors — streaming large result sets, locally and remotely.
- PDO — the persistent domain object pattern this module implements.
- Operations — operations backed by
AbstractPersistentOperation. - Persistence Wurblets — generates the per-entity persistence code that extends this module.
- Model Definition — where entities, relations, and attributes are declared.
- Session — the session/transaction/tracking context the persistence layer runs in.
- Tentackle SQL — the dialect-aware backend that builds the executed SQL.
- Services / ServiceFinder — the SPI mechanism behind the
@Service-replaced singletons and delegate discovery.