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:

  • Db — a persistence session: the implementation of Session that owns transactions, connections, prepared statements, id-sources, and statistics.
  • AbstractDbObject — a 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).
  • MpxConnectionManager — multiplexing 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.)
  • XaConnectionManager and XaMpxConnectionManager (module tentackle-jta) — the default and the multiplexing manager with connections that can be enlisted in global transactions.
  • ManagedConnection — a JDBC connection wrapped with a prepared-statement cache and SQLException → PersistenceException translation; also exposes the backend's database version via getBackendVersionInfo().
  • 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). An application implementing ConnectionManagerProvider (such as a server application with a multiplexing manager) supplies the default connection manager for new sessions; otherwise, a DefaultConnectionManager is used.

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, used by cascaded remote servers.
  • DbPoolFactory (@Service(SessionPoolFactory.class)) creates both kinds of pools; each pooled session lives in a DbPoolSlot, which removes closed sessions from the pool and keeps idle remote sessions alive.
  • RemoteSessionFactory, implemented by DbRemoteSessionFactory (@Service(RemoteSessionFactory.class)), and RemoteSessionAdapter provide the remote-session side.

Backend identity

An application may be connected to more than one database at the same time — a replication or migration tool, a server that reads from one database and writes to another, a test that compares two of them. Some state of this layer is shared by all sessions of the JVM, but belongs to exactly one physical database. That state needs a key telling the databases apart, and comparing a BackendInfo on every access would be too slow for it.

That key is the backend identity: an int returned by Db#getBackendIdentity() that denotes the physical backend the session is connected to. It is resolved once, when the Db is created, by DbUtilities#getBackendIdentity(BackendInfo):

  • By default, every distinct combination of backend type and URL gets the next free number, starting at 1 in the order of first use.
  • Remote sessions have the identity 0: there is no physical backend on their side of the wire.
  • The identity is part of the session name (Db7i2c41: instance 7, backend identity 2, connection 41), so it shows up in logs and exceptions.

The identity is only meaningful within the running JVM. The numbers depend on the order in which the backends are opened, so they must not be persisted or sent to another JVM.

Two things are keyed by it:

Why it matters

The identity can be wrong in two directions, and they are not equally bad:

  • Two databases, one identity. ObjectSequenceId would hand out ids in database B from a range it reserved in the sequence of database A. Sooner or later B's own sequence reaches that range and the ids collide. This must never happen.
  • One database, two identities. The id ranges are merely kept twice, which wastes a few ids but is safe. The modification logs, however, break: a serializer such as TripModificationLogSerializer spans its dictionary over many modlogs of one backend. If the sessions of the second identity have no serializer, their modlogs carry no object data; if they have their own, two independent dictionaries are interleaved in the same modlog table and the receiver cannot decode them.

Setting it manually

The default cannot know that two different URLs lead to the same database. That is the case whenever the application reaches one database in more than one way, for example:

  • with different URL options (a read-only connection next to the regular one, another ApplicationName, different timeouts or SSL settings),
  • via a host name in one configuration and an IP address or alias in the other,
  • directly and through a connection pooler or proxy,
  • via a JNDI DataSource and a JDBC URL, or two JNDI names.

In these cases assign the same identity to all of them, either with the identity backend property or programmatically via BackendInfo#setBackendIdentity(int):

# backend.properties
url=jdbc:postgresql://db.example.com/erp
identity=1001

# readonly.properties: the same database
url=jdbc:postgresql://db.example.com/erp?readOnly=true
identity=1001

Rules:

  • Use high or negative numbers. The generated identities count up from 1, so a low number may collide with one that was assigned automatically to a different database. 0 means "not set".
  • Set it before the first session of that URL is created. The identity is cached per backend type and URL, so the first session decides and later changes of the BackendInfo are ignored.
  • Give the same identity only to URLs that really are the same database. If in doubt, leave it unset: two identities for one database are the harmless mistake as long as no modlog serializer is involved.
  • An explicit identity can join URLs, but it cannot split one: sessions with the same backend type and URL always share an identity, even if they log in as different database users. Where the user decides in which schema the unqualified sequence name is resolved (as in Oracle), such sessions need URLs that differ.

Why it cannot be detected automatically

It is tempting to let the framework find out by itself whether two connections end up in the same database. There is no way to do that reliably, and a wrong "yes, the same" is exactly the dangerous direction described above:

  • A value stored in the database is copied along with the database. A marker row with a UUID, the current value of the object sequence, the highest object id, the contents of some table, the catalog's object ids, even the vendor's own system identifier: all of it is data, and all of it survives a backup that is restored somewhere else, a file-system snapshot, a cloned cloud instance or a promoted standby. The original and its copy then report the same value, although they are two physical databases with independent sequences. This is not an exotic case: applications that connect to two databases at once are typically the very ones that deal with a database and its copy — production and the restored test system, the old and the new side of a migration, the source and the target of a replication.
  • What the server reports about itself depends on the path. Host, address and port differ behind proxies, poolers, load balancers, NAT and after a failover, they are backend-specific, and one server hosts many databases anyway.
  • A probe lock is not safe either. Taking a lock through one connection and testing it through the other only shows that somebody holds that lock right now. A second JVM running the same probe with the same well-known lock key against the copy yields a false positive. A read-only connection or a standby cannot take the lock at all, and a pooler in transaction mode does not keep session-level locks. The probe needs write privileges and backend-specific SQL, has to be repeated against every backend already known, and its lock waits and timeouts would make opening a session slow and flaky.
  • There may be no connection to ask. The identity is needed when the Db is created. With a multiplexing connection manager, a connection is attached only later, and not necessarily the same one each time.

Which URLs denote the same database is a fact of the deployment topology, known to whoever configures the application and to nobody else. Tentackle therefore derives the identity from the only thing that is deterministic — the configuration — and lets the configuration override it.

Id sources

IdSource supplies unique object IDs. 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. The first id of a transaction locks the counter row until the transaction ends, so transactions allocating ids from the same table are serialized by the database. The counting state belongs to the transaction, so one instance can be shared by all sessions. A rollback to a savepoint may undo the increment and release the row lock; the next id or the final write then increments the counter again and continues after whatever other sessions allocated in the meantime.
  • ObjectSequenceId — uses a database sequence, multiplied by a configurable factor with an in-range counter to cut round-trips. There is one instance per sequence name, shared by all sessions. The range taken from the sequence is kept per database, i.e., per backend identity, so applications connected to several databases never mix up their ranges.

AbstractIdSource is the shared base; selection/configuration goes through IdSourceConfigurator / DefaultIdSourceConfigurator (@Service(IdSourceConfigurator.class)). Failures are reported as IdSourceException; its subclass IdSourceEmptyException is provided for application-specific id sources with a limited range.

The default id source is chosen from the backend: sequence:object_sequence_id if it supports sequences, table:object_id otherwise. The idsource backend property selects another one, in the syntax of the DefaultIdSourceConfigurator:

Value Id source
table:<name> An ObjectId counting in table <name>.
sequence:<name> An ObjectSequenceId on sequence <name>, with the default multipliers 100 and 1.
sequence:<name>:<sequenceMult>:<globalMult> The same with explicit multipliers. The id is (sequenceValue * sequenceMult + counter) * globalMult, where the counter runs from 0 to sequenceMult - 1 before the sequence is read again. A globalMult greater than 1 leaves room for application-specific offsets.

Different entities may use different id sources: if a resource named after the persistent class (com/example/persist/InvoicePersistenceImpl.properties) exists and holds an idsource line, it overrides the session's id source for that class. The class-specific id source is configured once per backend. Otherwise, the class uses the default id source of the session that requests the id, so sessions on different backends or with a different idsource property each get the id source that fits. All users of the same sequence must configure the same multipliers, otherwise a PersistenceException is thrown.

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 joins (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. Prepared statements are cached per StatementKey, i.e. the global StatementId plus the using class. 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 the metrics per transaction name in TransactionStatisticsResults: duration, number of statements and rollbacks.

Global transactions

A local session can take part in global (distributed) transactions, configured by the jta backend property or Db.setGlobalTransactionMode(…). The GlobalTransactionMode is NONE (default), JOIN (join a global transaction of the current thread, if any) or REQUIRED (the outermost begin() starts a global transaction if none is running). The session talks to the transaction manager only through the GlobalTransactionSupport SPI and completes its part via a GlobalTransactionListener; the implementation, together with the XA connection managers, is provided by the optional module tentackle-jta. See Global Transactions — JTA and XA.

Idle transaction supervisor

A transaction holds its connection — and the row locks it acquired — until it is committed or rolled back. A transaction that is never finished, for example because the thread running it blocks on something outside the database, would hold them forever. The DbTransactionFactory therefore starts a daemon thread, the Transaction Supervisor, as soon as the first transaction of a local session is created. Remote sessions never start it.

Each time a session performs I/O, its transaction is marked alive. The supervisor wakes up every txIdleInterval milliseconds and clears the mark. A transaction that has not been alive for more than txIdleTimeout consecutive intervals is considered stuck: the supervisor logs a warning with the connection's diagnostic info (including its statement history), then rolls back and closes the JDBC connection at the lowest level — in a separate thread, so a hanging driver cannot block the supervisor. The thread stuck in the transaction gets an exception on its next database access. The supervisor also logs a transaction that is no longer referenced but was never finished (as SEVERE). When it is asked to stop — terminateHelperThreads() at application shutdown — it keeps supervising the transactions still running and terminates only after they have finished or timed out. The next transaction starts it again.

The defaults are an interval of 1000 ms and a timeout of 60 intervals, i.e. one minute. Application servers configure both from the txIdleInterval and txIdleTimeout backend properties; other applications use DbTransactionFactory.getInstance().setTxIdleInterval(…) / setTxIdleTimeout(…). A timeout of 0 or less turns the idle check off. Raise the timeout rather than turning it off if long-running batch transactions legitimately stay idle for a while, e.g. while waiting for an external system.

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 plus BEGIN/COMMIT for transaction logging and RESET for the serializer (see Modification logging); applications may add their own types via a replaced ModificationTypeFactory.

Modification logging

ModificationLog (table modlog) records object modifications for asynchronous database coupling, higher-level replication, audit, etc. Logs are produced through ModificationLogFactory / DefaultModificationLogFactory.

Optionally, the state of an inserted or updated object is stored in the modlog's objdata column as well, so the receiver does not have to load the object from the database. Serialization is enabled per physical backend by registering a ModificationLogSerializer via ModificationLogFactory.setSerializer(backendIdentity, serializer), where backendIdentity is the session's backend identity. The PDO-aware implementation is TripModificationLogSerializer in tentackle-persistence.

TripModificationLogSerializer keeps the object data compact with a TRIP dictionary that spans many modlogs: a type definition is sent only with the first object of that type, later objects refer to it by ordinal. The modlog-writing transactions need not be serialized, although a receiver sees the modlogs only once they are committed, i.e., in commit order:

  • Commit-gated dictionary. The entries a transaction adds stay private to it until its physical commit (ModificationLogSerializer.commit, invoked via DbUtilities.notifyCommit). A concurrent transaction needing the same type defines it once more under an ordinal of its own. A rollback, or a rollback to a savepoint, discards the transaction's private entries. Hence, a modlog refers only to entries defined earlier in its own transaction or by transactions that committed before it was serialized. Their modlogs have lower ids and are visible to every receiver that sees the referring modlog.
  • Epochs. The dictionary is reset after a number of objects or some time. The reset writes a RESET modlog whose id becomes the number of the new epoch, stored in front of each serialized object. The receiver keeps one dictionary per epoch, so a transaction that started before a reset but commits after it is still decoded correctly. It must therefore not reset its dictionaries when it encounters a RESET modlog.
  • No waiting at the receiver. Unlike a TRIP connection, the receiver's dictionary fails immediately on an unknown ordinal or epoch. This happens only if the receiver was restarted or discarded the epoch (see setMaxEpochs), and is reported by a RebuildEpochException carrying the epoch. The receiver then rebuilds that epoch via rebuildEpoch from the already processed modlogs with an id ≥ the epoch and retries.

Note: asynchronous coupling itself is out of scope. There are too many different scenarios — topology, transport, ordering, conflict resolution, error handling and recovery, to name a few — to cover them with a single generic solution. Tentackle therefore only provides the fundamental building blocks: the modlogs, their optional object data and the serializer SPI. Reading the modlogs, shipping them and applying them on the receiving side must be implemented at the application level.

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

The diagnostics are switched on by static setters, typically at application startup. The statistics application property enables a sensible subset on servers: statement, TRIP-invocation and transaction statistics, a statistics log every 30 seconds, and logging nested selects at INFO.

Setter Default Effect
ManagedConnection.setCollectingStatistics(boolean) off Count the executions and durations of each SQL statement.
ManagedConnection.setLogStatisticsMillis(long) 0 Log and clear the statement statistics periodically.
StatementStatistics.setWithParameters(boolean) off Separate the statistics per parameter values, not only per statement.
ManagedConnection.setLogLevelForParallelOpenResultSets(Level) null Log nested selects: a select executed while a result set of the same connection is still open — typically a lazy relation loaded inside a loop over a result set.
ManagedConnection.setTraceParallelOpenResultSets(boolean) off Also add such statements to the StatementTracer.
ManagedConnection.setMaxStatementsPerTransaction(int) 0 Fail a transaction that executes more statements than this — a guard against runaway loops exhausting memory or the backend.
ManagedConnection.setLogMinAttachMillis(long) 0 Log sessions that keep a connection attached longer than this; -1 logs every detach.
ManagedConnection.setMdcFilter(Pattern) null Only collect statements for logging if the thread's mapped diagnostic context matches.
ManagedConnection.setCheckingStatementResultSetParameters(boolean) off Verify that a reused prepared statement has the requested result set type and concurrency.
StatementHistory.setLogMinDurationMillis(long) 0 Log statements running longer than this as a warning.
DbTransactionFactory.setCollectingStatistics(boolean) off Collect transaction statistics (count, duration) per transaction name.
RemoteDbDelegateInvocationHandler.setCollectingStatistics(boolean) off Count and measure TRIP invocations per method on the server.
RemoteDbDelegateInvocationHandler.setLoggingInvocations(boolean) / setLoggingInvocationDetails(boolean) off Log each TRIP invocation, optionally with arguments and return value.
RemoteDbDelegateInvocationHandler.setLogMinDurationMillis(long) / setLogMinReturnedCollectionSize(int) 0 Log only invocations that took at least this long, or returned at least this many elements.
RemoteDbDelegateInvocationHandler.setMdcFilter(Pattern) null Only log TRIP invocations for matching mapped diagnostic contexts.

When a connection's diagnostic info is logged — for example by the idle transaction supervisor — it includes the statement history. Code stuck in a loop can execute the same statements many thousand times, so histories of 256 or more entries are compressed by the StatementHistoryCompressor: a block of up to 64 statements that repeats immediately is printed once, followed by the number of repetitions.

Within a transaction, every statement is kept in the history until the connection is released, including its parameters. For large transactions such as bulk inserts, this costs memory. Session.setStatementHistoryLimit(int) limits the history for the rest of the current transaction to its most recent statements (0 keeps none) — or declaratively via @Transaction(statementHistoryLimit = ...), see Transactions. The limit is reset when the transaction ends, so other transactions keep their full history. The dump then shows how many older statements were omitted. Dropped statements still count for the statement statistics, the transaction statistics, and setMaxStatementsPerTransaction.

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 by the handling of SQLExceptions wrapped in a PersistenceException: driver-specific exceptions are replaced by plain java.sql.SQLExceptions that remote clients can deserialize, transient errors flag the exception as temporary, and the messages carry SQL state and error code.

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. DbTripFactory (@Service(TripFactory.class)) installs the client-side invocation handler, which translates a vanished server-side delegate into a RemoteSessionClosedException. DbObjectResult carries the new id, serial and table serial of a remote insert/update/delete back to the client. 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).
  • JDBC drivers are supplied by the application. The tests run against H2; the PostgreSQL driver is a test dependency only used with -Ppostgres.
  • 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 (SessionFactory → DbFactory, SessionPoolFactory → DbPoolFactory, ModificationTracker → DbModificationTracker, SessionUtilities → DbSessionUtilities, DiagnosticUtilities → DbDiagnosticUtilities, PersistedPreferencesFactory → DbPreferencesFactory, TripFactory → DbTripFactory) and provides its own replaceable services (RemoteSessionFactory, IdSourceConfigurator, ModificationLogFactory, RemoteDbDelegateLocator, and the self-defaulting DbUtilities, DbClassVariablesFactory, DbTransactionFactory, ModificationTypeFactory, ManagedConnectionMonitor, StatementTracer, TableSerialHistory); 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.