Skip to content

Tentackle Database — The Low-Level Persistence Engine

Overview and Motivation

The tentackle-database module (Java package org.tentackle.dbms) is Tentackle's low-level persistence engine — the runtime that actually moves data between Java objects and a relational database. It sits between the dialect-aware tentackle-sql backend below it and the PDO-aware tentackle-persistence layer above it, and it provides the concrete implementation of the Session abstraction.

Its two central classes are:

  • Dba persistence session: the implementation of Session that owns transactions, connections, prepared statements, id-sources, and statistics.
  • AbstractDbObjecta persistent low-level database object: the base class every persistence implementation extends, providing generic insert/update/delete/select machinery driven by entity-specific code that the Wurbelizer generates from the model.

The defining feature of this layer is 2-tier / n-tier transparency. A Db session is an abstraction over a connection to a "server", where server need not be a database server at all:

  • A local session talks to a database backend through a ConnectionManager that owns the physical JDBC connection. This is what client apps in 2-tier mode and application servers use.
  • A remote session is connected to a Tentackle application server over TRIP. This is what n-tier (n ≥ 3) clients use.

Because both look identical to the code above, a Tentackle application switches between 2-tier and n-tier deployment by changing a config file, not the source code.

Design principles

  • Session over connection, not session = connection. A Db does not own a JDBC connection directly; it asks a ConnectionManager to attach one for the duration of a task and detach it afterward. This decoupling is what enables connection multiplexing in servers.
  • Generated specialization, generic engine. AbstractDbObject/AbstractDbOperation hold all the generic persistence logic; the per-entity SQL and column mapping are generated (by wurblets) into the application's subclasses. The OO "template method" approach keeps the framework code entity-agnostic.
  • Location transparency via TRIP delegates. Every persistence capability has a remote-delegate twin in org.tentackle.dbms.trip, so the same calls run locally or against a remote server.
  • SQLExceptions never leak. Statement/result wrappers and the session utilities translate vendor SQLExceptions into Tentackle's PersistenceException hierarchy, classified via the Backend, so upper layers never see raw JDBC errors.
  • Replaceable singletons via SPI. Utilities, factories, and the modification tracker are @Service-registered so the PDO layer above can swap in PDO-aware versions transparently.

Key Concepts

Db — the session

Db implements Session (from tentackle-session) and is the workhorse of the module. Each thread must use its own Db. Its responsibilities include:

Group Examples Purpose
Connection brokering getConnectionManager, attach/detach (internal), isRemote Acquire/release a managed connection for a unit of work.
Transactions begin, commit, rollback, createTransaction, savepoints Run (nestable) transactions; the voucher protocol from Session decides who really commits.
Statements getPreparedStatement, createStatement Obtain cached, wrapped statements bound to the session's connection.
Id sources the per-session IdSource (ObjectId / ObjectSequenceId) Allocate unique object IDs.
Batching executeBatch, batch configuration Collect prepared statements into JDBC batches for throughput.
Modification tracking the session's ModificationTracker integration Advance table serials on writes for caches/notifications.
Diagnostics statement history, statement/transaction statistics, the statement tracer Observe and profile database activity.
Keep-alive / lifecycle isAlive, reconnection policy Survive idle timeouts and dropped connections.

A Db can be cloned and pooled, and exposes whether it is local or remote so the rest of the framework can adapt where it genuinely matters (rarely).

AbstractDbObject and AbstractDbOperation

AbstractDbObject<P> is the base of all persistent objects. It is associated with a Session (local or remote) and provides the generic CRUD engine; entity specifics (table/column mapping, finders, relations) are generated by wurblets into subclasses according to the model. It carries the bookkeeping every persistent row needs — object id, serial (optimistic locking), table serial (cache expiry) — and is @TripSerializable so instances can travel between JVMs.

AbstractDbOperation<P> is the counterpart for operations — units of work that are not a single row but a (often complex, transactional) procedure bound to a Db session, and are likewise remoting capable.

Per-class static metadata lives in DbObjectClassVariables and DbOperationClassVariables, created via the DbClassVariablesFactory (which the PDO layer replaces with a PDO-aware factory).

Connection management

JDBC connections are never used directly. A session borrows one from a ConnectionManager via attach, uses it for a transaction/one-shot, then detaches it — letting the manager broker N sessions over M connections.

  • ConnectionManager — the broker interface (login/logout, attach/detach/forceDetach, getMaxConnections, …).
  • DefaultConnectionManager — strict 1:1 mapping (each session keeps its own physical connection).
  • MpxConnectionManagermultiplexing manager that maps N sessions onto M < N connections, transparently to the application; the typical choice for application servers with many clients. (Note: this is not session pooling — multiplexing is invisible to the app.)
  • ManagedConnection — a JDBC connection wrapped with a prepared-statement cache and SQLExceptionPersistenceException translation.
  • ManagedConnectionMonitor — watches managed connections; DefaultReconnectionPolicy decides how to reconnect after a failure.

Connection managers are not used by remote sessions (those go over TRIP instead).

Session creation and pooling

  • DbFactory (@Service(SessionFactory.class)) — creates Db sessions; it is the local implementation of the session factory contract.
  • DbPool — a SessionPool of Dbs for one SessionInfo. With the default connection manager each pooled Db is one physical connection; with the multiplexing manager pooled Dbs are virtual connections attached only during operations — the preferred server configuration.
  • MultiUserDbPool — a pool of DbPools, one per SessionInfo, for multi-user servers.
  • DbRemoteSessionFactory (@Service(RemoteSessionFactory.class)) and RemoteSessionAdapter provide the remote-session side.

Id sources

IdSource supplies unique object IDs (normally one instance per session). Two implementations ship:

  • ObjectId — used when the backend has no sequences; updates a counter table, optimized to write only once at the end of transaction.
  • ObjectSequenceId — uses a database sequence, multiplied by a configurable factor with an in-range counter to cut round-trips.

AbstractIdSource is the shared base; selection/configuration goes through IdSourceConfigurator / DefaultIdSourceConfigurator.

Statements, queries and result sets

  • StatementWrapper / PreparedStatementWrapper — wrap JDBC statements, catch/translate SQLExceptions, and guard against accidental reuse.
  • ResultSetWrapper — an AutoCloseable wrapper over a ResultSet; ResultSetSection / ResultSetSkipBlock support joines (due to inheritance or for load joins).
  • Query — builds the SQL string + parameter set for a one-off query (e.g., user-entered), adding the backend-specific SELECT/LIMIT/OFFSET automatically. All predefined datatypes are accepted as parameters, as well as application-specific types with a registered DataType. Notice that some datatypes span more than one column (BMoney or ZonedDateTime, for example) and thus require a matching number of question marks.
  • The org.tentackle.dbms.dsl package builds such a Query from typed expressions instead of hand-written SQL strings, and counts the columns of multi-column datatypes for you — see The Query DSL.
  • DbBatch / DbBatchStatement — JDBC batching of standard prepared statements, grouped per root-entity type to preserve referential integrity, flushed at savepoints/commit. Enabled via the batchsize backend property; off by default and to be used with care because it complicates error diagnosis. See SQL Statement Batching.

Transactions

AbstractTransaction is the serializable base; DbTransaction holds local transaction state and RemoteTransaction the remote variant. DbTransactionFactory creates them and DbTransactionHandle is the caller's handle. TransactionStatistics collects per-transaction metrics.

Modification tracking and table serials

This layer implements the change-tracking contracts from tentackle-session:

  • DbModificationTracker (@Service(ModificationTracker.class)) — the concrete tracker; advances serials when data changes and notifies listeners (e.g., caches).
  • DbModification (table modification) and ModificationTally — the persisted per-table modification serial and its in-memory counter (local sessions only).
  • TableSerialHistory and TableSerialExpirationBacklog — track committed inserts/updates/deletes in memory so caches can be expired precisely, filling the gaps left by deletions and rolled-back changes and avoiding needless cache invalidation. Maintained only by the server connected to the database; intermediate servers and clients fetch their expiration sets remotely.
  • DbModificationType / ModificationType / ModificationTypeFactory — enumerate INSERT/UPDATE/DELETE (and friends).

Modification logging

ModificationLog (table modlog) records object modifications for asynchronous database coupling, higher-level replication, audit, etc. Objects opt in via ModificationLoggable; logs are produced through ModificationLogFactory / DefaultModificationLogFactory.

Persistence visitors

PersistenceVisitor is a reflective hook registered on a Db (valid for one transaction) that is consulted before each INSERT/UPDATE/DELETE to decide whether the operation is allowed or should be silently skipped — e.g. IgnoreDuplicatesPersistenceVisitor.

Diagnostics and statistics

Utilities (SPI replacements)

  • DbUtilities (@Service(DbUtilities.class)) — persistence helpers; the clean seam between the low- and high-level layers (the PDO layer replaces it with PersistenceUtilities).
  • DbSessionUtilities (@Service(SessionUtilities.class)) — extends the session utilities to map SQLExceptionPersistenceException.

Database-backed Preferences

The org.tentackle.dbms.prefs package implements the standard java.util.prefs.Preferences API with the database as the backing store, so application preferences are shared across all JVMs rather than living in a local registry/file:

  • DbPreferences — the Preferences implementation; nodes persist via DbPreferencesNode and key/value pairs via DbPreferencesKey; changes are flushed through DbPreferencesOperation and node/key events propagate to all JVMs.
  • DbPreferencesFactory (@Service(PersistedPreferencesFactory.class)) — a drop-in replacement for the JRE preferences factory, normally reached via PersistedPreferencesFactory.getInstance().
  • prefs/trip — the remote delegate so preferences work over a remote connection.

Remoting (TRIP)

The org.tentackle.dbms.trip package makes the whole layer location transparent. The connection/handshake chain is:

  • TripServer — the generic application server; parses the service=<uri> backend properties and creates the TRIP transports that accept client connections.
  • RemoteDbConnection — the first remote object handed to a connecting client; handles login to a session.
  • RemoteDbSession — the server-side session that creates all other delegates for the client; cleaned up by RemoteDbSessionCleanupThread.

Each capability is exposed as a remote delegate (interface + …Impl executor):

Delegate / impl Mirrors
DbRemoteDelegate / …Impl Db (the session itself)
AbstractDbObjectRemoteDelegate / …Impl AbstractDbObject CRUD/queries
AbstractDbOperationRemoteDelegate / …Impl AbstractDbOperation
DbModificationTrackerRemoteDelegate / …Impl modification tracking / serials
ModificationLogRemoteDelegate / …Impl modification logs

RemoteDbDelegateLocator / DbRemoteDelegateLocator (@Service(RemoteDbDelegateLocator.class)) find the right delegate for a class; the invocation handlers (DbRemoteInvocationHandler, RemoteDbDelegateInvocationHandler) and RemoteDbSessionInvocationFilter route and guard the calls. SQLExceptionComponentProvider / SQLExceptionInstanceCreator ensure JDBC exceptions serialize correctly across the wire, and DefaultMasterSerial carries the master modification serial back to clients.

Preloading remote delegates

Delegates are created lazily: the first time a class needs one, Db.getRemoteDelegate(int) asks the server for it — one network roundtrip per class. For an application with several hundred entities running on a link with a high latency, these roundtrips accumulate and dominate the perceived startup time.

A class can be marked as a candidate for preloading with @PreloadRemoteDelegate:

@PersistentObjectService(User.class)
@PreloadRemoteDelegate
public class UserPersistenceImpl extends AbstractPersistentObject<User, UserPersistenceImpl> { ... }

The annotation is meta-annotated with @Service, so the build collects the annotated class names in META-INF/services/org.tentackle.dbms.trip.PreloadRemoteDelegate. The client reads them as plain strings via DbUtilities.getPreloadDelegateClassNames() — the classes themselves are never loaded — and fetches all of their delegates in a single roundtrip using RemoteDbSession.getRemoteDelegates(String[]):

Session session = Pdo.createSession(sessionInfo);
session.preloadRemoteDelegates();       // one roundtrip instead of one per class

Preloaded delegates are consumed transparently: getRemoteDelegate(int) takes them from the preload map instead of going to the server. Preloading is remembered for the session, so it is repeated on reOpen() and after an automatic reconnection. A cloned session is a session of its own and does not inherit the request — it must invoke preloadRemoteDelegates() itself if it wants to.

Two rules apply:

  • The application decides, per session. Nothing is preloaded unless preloadRemoteDelegates() is invoked. Not every session benefits — preloading costs one roundtrip plus one server-side delegate per candidate, which is wasted on short-lived or special-purpose sessions.
  • Annotate the classes used at startup, not the whole entity model, and never a class whose delegate changes the state of the server session when created. DbModificationTracker is such an example: creating its delegate marks the session as the modification tracker's session, so it must not be a preload candidate.

Class names the server cannot resolve are answered with null and logged there — preloading never breaks the session. Since getRemoteDelegates is part of the RemoteDbSession interface, client and server must be built from the same Tentackle version, which the login handshake verifies anyway.


Package Layout

Package Contents
org.tentackle.dbms The engine: Db, AbstractDbObject/AbstractDbOperation, connection managers and ManagedConnection, pools and factories, id-sources, statement/query/result wrappers, transactions, batching, modification tracking + table serials, modification logging, persistence visitors, diagnostics/statistics and the SPI utility replacements.
org.tentackle.dbms.dsl The query DSL: typed tables, columns, expressions and conditions rendered to the SQL of a Query.
org.tentackle.dbms.prefs (+ prefs.trip) Database-backed java.util.prefs.Preferences and its remoting.
org.tentackle.dbms.trip The TRIP server, remote connection/session and the remote delegates that make the layer location-transparent.
org.tentackle.dbms.service The Hook (ModuleHook) for i18n bundle resolution.

How It Fits Together

A persistence operation flows through the layers as follows:

   tentackle-persistence  (AbstractPersistentObject — PDO-aware)
        │  extends
   AbstractDbObject ──── runs against ──── Db (Session)
                          local ────────────┤──────────── remote
                            ▼               │                ▼
                     ConnectionManager      │        DbRemoteDelegate (TRIP)
                            │               │                ▼ (on server)
                     ManagedConnection      │        …RemoteDelegateImpl runs the
                            │               │        same AbstractDbObject logic
                     Backend (tentackle-sql)│        against a local Db
                            │               │                │
                          JDBC  ◄───────────┘                ▼
                                                       results via TRIP

For a local Db, AbstractDbObject builds SQL via the Backend, borrows a ManagedConnection from the ConnectionManager, executes through the statement wrappers, and maps the ResultSetWrapper back to objects. For a remote Db, the call is routed by the delegate locator to a …RemoteDelegate; on the server the matching …RemoteDelegateImpl runs the very same AbstractDbObject logic against its own local Db and returns the results over TRIP. Either way every write advances a table serial in the DbModificationTracker, which caches and remote clients consult (via the expiration backlog) to invalidate exactly the stale entries.

The per-entity classes that extend AbstractDbObject/AbstractDbOperation are generated by wurblets from the model; this module is the generic runtime they plug into.


Module Dependencies

tentackle-database sits in the middle of the persistence stack:

  • It requires transitive tentackle-session (the Session contract it implements) and tentackle-sql (the dialect-aware Backend it executes), plus java.naming.
  • It bundles the PostgreSQL JDBC driver for tests; production drivers are supplied by the application.
  • It opens org.tentackle.dbms and org.tentackle.dbms.prefs to org.tentackle.core for TRIP serialization.
  • It provides a ModuleHook service (org.tentackle.dbms.service.Hook) for i18n.
  • Through @Service, it replaces several lower-layer singletons (SessionFactoryDbFactory, ModificationTrackerDbModificationTracker, SessionUtilitiesDbSessionUtilities, DiagnosticUtilitiesDbDiagnosticUtilities, PersistedPreferencesFactoryDbPreferencesFactory); the PDO layer above in turn replaces DbUtilities/DbClassVariablesFactory with PDO-aware versions.
  • Session — the Session/transaction/tracking contracts this module implements.
  • The Query DSL — building projection queries from typed expressions instead of SQL strings.
  • Tentackle SQL — the dialect-aware backend that builds the executed SQL.
  • Persistence — the PDO-aware layer that builds on AbstractDbObject/Db.
  • PDO — the persistent domain object pattern at the top of the stack.
  • TRIP — the remoting protocol behind the *.trip delegates.
  • Model Definition — where the entities whose persistence code extends this module are declared.
  • Services / ServiceFinder — the SPI mechanism behind the @Service-replaced singletons and delegate discovery.