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 ofSessionthat 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
ConnectionManagerthat 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
Dbdoes not own a JDBC connection directly; it asks aConnectionManagerto 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/AbstractDbOperationhold 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'sPersistenceExceptionhierarchy, classified via theBackend, 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.)XaConnectionManagerandXaMpxConnectionManager(moduletentackle-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 andSQLException→PersistenceExceptiontranslation; also exposes the backend's database version viagetBackendVersionInfo().ManagedConnectionMonitor— watches managed connections;DefaultReconnectionPolicydecides 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)) — createsDbsessions; it is the local implementation of the session factory contract.DbPool— aSessionPoolofDbs for oneSessionInfo. With the default connection manager each pooledDbis one physical connection; with the multiplexing manager pooledDbs are virtual connections attached only during operations — the preferred server configuration.MultiUserDbPool— a pool ofDbPools, one perSessionInfo, used by cascaded remote servers.DbPoolFactory(@Service(SessionPoolFactory.class)) creates both kinds of pools; each pooled session lives in aDbPoolSlot, which removes closed sessions from the pool and keeps idle remote sessions alive.RemoteSessionFactory, implemented byDbRemoteSessionFactory(@Service(RemoteSessionFactory.class)), andRemoteSessionAdapterprovide 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:
ObjectSequenceIdkeeps the id range it took from the sequence per backend identity (see Id sources).ModificationLogFactorykeeps oneModificationLogSerializerper backend identity (see Modification logging).
Why it matters¶
The identity can be wrong in two directions, and they are not equally bad:
- Two databases, one identity.
ObjectSequenceIdwould 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
TripModificationLogSerializerspans 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 samemodlogtable 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
DataSourceand 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.
0means "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
BackendInfoare 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
Dbis 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/translateSQLExceptions, and guard against accidental reuse.ResultSetWrapper— anAutoCloseablewrapper over aResultSet;ResultSetSection/ResultSetSkipBlocksupport 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-specificSELECT/LIMIT/OFFSETautomatically. All predefined datatypes are accepted as parameters, as well as application-specific types with a registeredDataType. Notice that some datatypes span more than one column (BMoneyorZonedDateTime, for example) and thus require a matching number of question marks.- The
org.tentackle.dbms.dslpackage builds such aQueryfrom 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 perStatementKey, i.e. the globalStatementIdplus the using class. Enabled via thebatchsizebackend 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(tablemodification) andModificationTally— the persisted per-table modification serial and its in-memory counter (local sessions only).TableSerialHistoryandTableSerialExpirationBacklog— 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 replacedModificationTypeFactory.
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 viaDbUtilities.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
RESETmodlog 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 aRESETmodlog. - 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 aRebuildEpochExceptioncarrying the epoch. The receiver then rebuilds that epoch viarebuildEpochfrom 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¶
StatementTracer— on-demand SQL tracing (off by default for performance), withStatementTraceException.StatementStatistics/StatementStatisticsResultandStatementHistory— collect and report statement usage.DbDiagnosticUtilities(@Service(DiagnosticUtilities.class)) — adds persistence info to thread stack dumps.
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 withPersistenceUtilities).DbSessionUtilities(@Service(SessionUtilities.class)) — extends the session utilities by the handling ofSQLExceptions wrapped in aPersistenceException: driver-specific exceptions are replaced by plainjava.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— thePreferencesimplementation; nodes persist viaDbPreferencesNodeand key/value pairs viaDbPreferencesKey; changes are flushed throughDbPreferencesOperationand node/key events propagate to all JVMs.DbPreferencesFactory(@Service(PersistedPreferencesFactory.class)) — a drop-in replacement for the JRE preferences factory, normally reached viaPersistedPreferencesFactory.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 theservice=<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 byRemoteDbSessionCleanupThread.
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.
DbModificationTrackeris 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(theSessioncontract it implements) andtentackle-sql(the dialect-awareBackendit 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.dbmsandorg.tentackle.dbms.prefstoorg.tentackle.corefor TRIP serialization. - It provides a
ModuleHookservice (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-defaultingDbUtilities,DbClassVariablesFactory,DbTransactionFactory,ModificationTypeFactory,ManagedConnectionMonitor,StatementTracer,TableSerialHistory); the PDO layer above in turn replacesDbUtilities/DbClassVariablesFactorywith PDO-aware versions.
Related Documentation¶
- 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
*.tripdelegates. - 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.