Persistence.java
/*
** Module : Persistence.java
** Abstract : Provides persistence services using FWD ORM
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------
** 001 ECF 20050830 @22419 Created initial version. Provides bootstrap
** services to configure a Hibernate session
** factory per database.
** 002 ECF 20050902 @22480 Added essential query services; refactored.
** This includes scrollable cursor queries,
** individual record loads into a re-used data
** model object, and enforced, unique queries.
** Refactored all bootstrap and database
** initialization logic into external class
** (DatabaseManager).
** 003 ECF 20051104 @23254 Changes to support locking and transactions.
** Added methods to lock records and query lock
** status, and to begin, commit, and roll back
** transactions.
** 004 ECF 20051109 @23382 Changed use paradigm from all static methods
** to getting an instance by factory. Previously
** all methods were static and required a
** physical database name to be specified. Now
** one instance is created and cached per
** physical database. Client code retrieves an
** instance via a factory method and operates on
** that instance. Instances are immutable and
** shared across multiple contexts. Added method
** to retrieve the next unique primary key using
** a database sequence.
** 005 ECF 20051215 @23751 Added support for ambiguous check. Also made
** primary key (phase 1) searches read-only.
** 006 ECF 20060120 @24044 Integrated with P2J server. Replaced
** ThreadLocal with ContextLocal.
** 007 ECF 20060130 @24354 Added temp table support. Required a new
** list() method, disabling the lock manager for
** temp table cases, and enabling arbitrary SQL
** statements to be executed (for temp table
** creation and dropping).
** 008 ECF 20060218 @24720 Created uniqueResultViolation method. This
** processing was needed from multiple places,
** including outside this class.
** 009 ECF 20060306 @25016 Added loadReadOnly method. Supports internal
** queries used for persistence framework
** housekeeping, including resolving foreign
** records using legacy keys (for natural join
** reverse resolution).
** 010 ECF 20060329 @25255 Fixed executeSQL method. Was passing the SQL
** string to PreparedStatement's execute method.
** 011 ECF 20060602 @26943 Optimization. HQL queries now will always
** attempt to load the full record, rather than
** just the primary key ID. This avoids the
** overhead of a separate load step.
** 012 ECF 20060608 @27001 Changed list() method signature and updated
** implementation. Number of results can now be
** limited and results can be set read-only.
** 013 ECF 20060610 @27098 Consolidated lock processing. Internal lock
** processing now uses public lock() method.
** 014 ECF 20060621 @27505 Changed session management and transaction
** implementation. Sessions are now always
** created upon demand, and detached records
** from a previous session are immediately
** associated with the new session. The begin
** transaction API is more lenient, reporting if
** a new transaction could not be created,
** rather than throwing an exception.
** 015 ECF 20060627 @27590 Improved temp table support. Always flush
** Hibernate session before executing JDBC
** statements directly. Ensure save and delete
** operations are executed within transactions.
** 016 ECF 20060712 @28050 Added trimTextArgs. Trims trailing whitespace
** from any query substitution parameter which
** is of type com.goldencode.p2j.util.character.
** 017 ECF 20060719 @28100 Fixed memory leak. Implemented cleanup()
** method for Hibernate Session's ContextLocal
** variable to ensure Session is explicitly
** closed when context ends. Although the P2J
** server would not pin the Session's memory
** directly, there was still a reference within
** Hibernate, such that the Session data could
** not be garbage collected after the client
** context ended.
** 018 ECF 20060719 @28101 Enable result set 'paging'. The list() method
** now accepts a startOffset parameter.
** 019 ECF 20060720 @28115 Integrated ChangeBroker. Enables optimization
** of avoiding unnecessary auto-flushing of the
** active Hibernate session. This dramatically
** improves the performance of RandomAccessQuery
** when the session contains a large number of
** entities.
** 020 ECF 20060725 @28187 Consolidated context local variables within
** the Context inner class. Also added a
** mechanism to prevent sessions from being
** closed while external entities are dependent
** upon them.
** 021 ECF 20060728 @28269 Made queries cacheable; improved reliability
** of locking in dynamic retrieval mode.
** 022 ECF 20060828 @28930 Better error handling. In some cases, session
** was not being closed after catching an
** unrecoverable exception.
** 023 ECF 20060830 @29005 Fixed getQuery(). In case where we were not
** explicitly paging results, the query would
** return an empty result set, due to incorrect
** maxResults setting.
** 024 ECF 20060901 @29136 Fixed memory leak. decrementSessionsUsers()
** was actually incrementing the session user
** count, due to a typo.
** 025 ECF 20060903 @29203 Fixed Hibernate assertion failure. Ensure
** session is flushed before an object is
** evicted.
** 026 ECF 20060904 @29219 Better error reporting. scroll() reports HQL
** statement and context info on query error.
** 027 GES 20060905 @29230 Updated trimTextArgs() to truncate any
** character vars that have embedded null bytes.
** This ensures that null bytes can never be
** passed into queries as substitution
** arguments.
** 028 ECF 20060911 @29460 Fixed query defect. Restore default query
** settings when checking out a cached query.
** 029 ECF 20060919 @29697 Made mods required by changes to ChangeBroker
** calls. Also added trace debug output during
** record eviction.
** 030 ECF 20060929 @30035 Fixed out of range query substitution date
** parameters. We now 'clip' those dates to the
** minimum and maximum date values supported by
** the backing database and log a debug message.
** Renamed trimTextArgs() method to
** preprocessQueryParameters() to reflect
** broader purpose.
** 031 ECF 20061010 @30309 Made changes to accommodate auto-commit temp
** tables. These were primarily side effects of
** changes made to TemporaryBuffer and
** RecordBuffer. A new variant of the static
** factory method to retrieve instances of this
** class was added. The new version of get()
** will create a secondary instance of this
** class, backed by the same database as the
** primary instance. This allows auto-commit and
** non-auto-commit temp tables to share the same
** physical backend, but maintain separate
** connections and Hibernate sessions.
** 032 ECF 20061012 @30390 Fixed nonthreadsafe access error. Flush
** session to ensure consistency, when a request
** to close the session cannot be honored
** immediately (because a query is pinning it
** open).
** 033 ECF 20061014 @30527 Integrated SessionListener architecture.
** Objects which register as SessionListeners
** are notified when a Hibernate session is
** about to close. Deprecated session user count
** mechanism which would pin open a session as
** long as a reference count of dependents was
** non-zero.
** 034 ECF 20061110 @31094 Improved logging. Errors reassociating
** detached records are logged in detail to
** prevent these errors from being masked in
** silent error mode.
** 035 ECF 20061113 @31140 Added new variant of load(). Needed for cases
** where a RecordBuffer instance is not readily
** available.
** 036 ECF 20061116 @31236 Minor adjustments to exception handling.
** Ensure session gets closed when a database
** error occurs.
** 037 ECF 20061120 @31325 Changed session closing behavior. Do not
** close temp table sessions if the temp table
** count exceeds zero (with no database errors).
** 038 ECF 20061212 @31667 Fixed context cleanup. Inner class Context's
** cleanup was not properly connected to end of
** context cleanup, so it was never being
** invoked.
** 039 ECF 20070111 @31779 Added flushBatch method. Flushes a batch of
** records to the database and clears the active
** session.
** 040 ECF 20070130 @32039 Various cleanup. Ensure session is flushed if
** necessary after attaching a record. Ensure
** session is closed for all Hibernate exception
** processing. Ensure session is closed if a
** SessionListener throws an exception during
** close notification. Deprecated method which
** creates secondary Persistence instance for a
** single database.
** 041 ECF 20070212 @32155 Integrated DirtyReadManager. This allows DMO
** validation to check uncommitted updates
** across client contexts. Also added the
** ability to temporarily disable flushing,
** which was necessary to correct a flaw in
** validation processing.
** 042 ECF 20070228 @32246 Modified HQL-based load() method. It can now
** handle either an ID-only projection query or
** a query which fetches and returns a DMO
** directly. In the latter case, the additional
** lock() and get() steps are omitted.
** 043 ECF 20070326 @32611 Added bulk delete method. Deletes all records
** which meet the specified criteria for a given
** DMO type.
** 044 ECF 20070329 @32688 Fixed attach/detach. Flush check was not
** correct.
** 045 ECF 20070412 @32983 Retrofitted locking to use RecordLockContext.
** An instance of this class is created for each
** context, one per database. Simplified context
** cleanup.
** 046 ECF 20070416 @33025 Integrated user interrupt handling.
** 047 ECF 20070508 @33458 Added queryIndexData(). Uses database
** metadata to inspect indices on a given table.
** 048 ECF 20070514 @33485 Fixed queryIndexData(). In some cases, root
** column name must be extracted from string
** queried from index metadata in a dialect
** specific way.
** 049 ECF 20070516 @33669 Fixed abend caused by an orphaned transaction
** at session close. When a session is closed
** due to an error, roll back any open
** transaction before closing the session. Also
** made error logging less noisy at this layer,
** since a severe error usually will be logged
** in error handling provided by users of the
** persistence framework. Details are still
** logged at FINE logging level.
** 050 ECF 20070519 @33682 Fixed load() method (variant which loads by
** primary key). Ensure session is flushed if
** necessary and use Session.get() instead of
** Session.load(), in case record was deleted.
** 051 ECF 20070602 @33926 Integrated new scope options of
** SessionListener. GLOBAL_SCOPE registers a
** session listener cleanup routine with the
** transaction manager global scope. NO_SCOPE
** does not register a cleanup method at all.
** 052 EVL 20070609 @34005 Adding explicit import of the class
** org.hibernate.type.Type to eliminate conflict
** with the same class from java.lang.reflect
** package to be able to compile for Java 6.
** 053 ECF 20070703 @34360 Removed IllegalStateException from flush().
** Now throws recoverable PersistenceException
** instead when no session is available.
** 054 ECF 20070706 @34401 Implemented SessionListener deregistration
** notification.
** 055 ECF 20070708 @34414 Optimization. Cache ChangeBroker in context
** local state, rather than retrieving it every
** time flushing is checked.
** 056 ECF 20070709 @34419 Optimization. Consolidated access to Context
** instance to prevent multiple ContextLocal
** lookups within the same methods. Changed some
** signatures and added/removed several private
** methods to accomodate this.
** 057 ECF 20070720 @34620 Modified implementation of attach(). It now
** associates a detached DMO with the current
** Hibernate session.
** 058 ECF 20070812 @34857 Changed error message on non-unique result
** exception. Report buffer name rather than
** DMO name.
** 059 ECF 20070817 @34876 Fixed all variants of load() to prevent stale
** data retrieval. There were cases where a DMO
** instance in the Hibernate session cache would
** become stale based on an update in another
** user context. We now force a refresh from the
** database when reading a record with a lock.
** 060 ECF 20070901 @35302 Changed executeSQL() method. Connection is
** now set to autocommit=true before the SQL
** statement is executed. Added new method
** executeSQLQuery() to allow execution of
** arbitrary SQL queries with no change to
** connection's autocommit setting. Replaced use
** of database name string with database info
** object. Added trace logging of execution time
** of queries and other database accesses.
** 061 ECF 20071002 @35331 Implemented dialect-specific callback hooks.
** Registered P2JDialect is given a chance to
** perform post-transaction processing with
** dialect-specific data stored in the current
** client context.
** 062 ECF 20071008 @35399 Added back convenience method get(String).
** 063 ECF 20071008 @35445 Fixed regression introduced by #060 (@35302).
** Incorrect LockManager and DirtyReadManager
** instances were being created for the temp
** table database.
** 064 ECF 20071018 @35575 Refactored to enable creation by factory.
** Moved static factory methods to new class
** PersistenceFactory. Also, lock manager and
** dirty read manager classes are now read from
** the directory instead of hard-coded.
** 065 ECF 20071221 @36545 Fixed load() method implementations. We now
** catch UnresolvableObjectException, which is a
** recoverable condition which can occur when
** invoking Session.refresh(). This will be
** thrown by Hibernate when we have a DMO in the
** first level cache, which has been deleted
** from the database.
** 066 SVL 20071231 @36626 Identity manager is initialized to support
** primary key generation on a per-table basis.
** 067 ECF 20080102 @36644 Removed deprecated nextPrimaryKey() method.
** Replacement is nextPrimaryKey(String) added
** in #066 (@36626).
** 068 ECF 20080108 @36733 Fixed bulk delete. Removed optimization of
** query flush, which was causing a
** StaleStateException in cases of deleted DMOs
** with collection associations.
** 069 SVL 20080111 @36772 getIdentityManager() function has been added.
** 070 ECF 20080129 @37004 Added debug logging. Log DMO persist event.
** 071 ECF 20080312 @37480 Integrated support for embedded H2 database.
** Also changed return type of list() method to
** be less specific.
** 072 ECF 20080403 @38136 Use entity name instead of DMO class to test
** if flush is required. Added load() variant
** which accepts entity name instead of DMO
** class.
** 073 SVL 20080421 @38097 Added foreignKeysEnabled property.
** 074 CA 20080430 @38160 Added a static method retrieveIndexData which
** contains the queryIndexData's code related to
** building the index list from a result set.
** 075 ECF 20080606 @38642 Removed ValidationContext references.
** 076 ECF 20080611 @38697 Major refactoring. Index metadata query
** methods were rationalized to be accessible in
** more situations. Added methods to temporarily
** "borrow" a connection from a current session.
** Removed all references to the obsolete
** DirtyReadManager class hierarchy.
** 077 ECF 20080624 @38918 Optimized column type metadata methods used
** primarily at startup. This approach avoids
** many round trips to the database to query
** column metadata, but may not work with all
** JDBC drivers. In case not, the original,
** unoptimized approach is used.
** 078 CA 20080605 @38566 Renamed method delete to deleteOrUpdate.
** 079 ECF 20080806 @39324 Added rowid support. Changed default identity
** manager to SequenceIdentityManager, which
** generates database-wide unique IDs. Primary
** keys are now 64-bit integers.
** 080 CA 20080815 @39462 Support API change in DBUtils. Fixed NPE in
** getColumnDataType.
** 081 SVL 20080901 @39652 Identity pool is initialized together with
** identity manager.
** 082 ECF 20080923 @39936 Memory tuning. Do not cache queries used for
** deletes/updates.
** 083 ECF 20080924 @39946 Added load(RecordIdentifier). Loads a single
** record, given its unique identifier.
** 084 ECF 20081015 @40091 Refactored query substitution parameter
** preprocessing. Abstracted out a static method
** to process a single parameter, so it is
** accessible to other classes in the package.
** 085 ECF 20081021 @40208 Force refresh of DMOs in active buffers when
** getting a new Session after a rollback. This
** ensures active buffers have current versions
** of these records.
** 086 ECF 20081024 @40224 Fixed regression caused by #085 (@40208). If
** a record in a buffer was deleted during a
** rollback, we must handle this stale state
** when creating the new session and refreshing
** records left in active buffers.
** 087 ECF 20081106 @40354 Modified directory lookups for lock manager
** and identity manager. Changed path to allow
** for other, related properties to be set in
** the directory.
** 088 ECF 20081118 @40549 Minor change to error message text.
** 089 ECF 20081204 @40803 Added performance warning logging. Queries
** which take longer than certain thresholds
** (configured in the directory) trigger a
** warning log entry.
** 090 ECF 20090114 @41162 Fixed load() (HQL variant). Was not correctly
** unlocking when passed a lock type of NONE.
** 091 ECF 20090128 @41239 Improved Context.getSession(). When
** associating the current records of active
** buffers with a new session after a rollback
** of the previous session, we only refresh
** those we know to be stale, rather than all
** of them. This reduces the number of trips
** to the database. The rolledBack parameter to
** Context.closeSession() was removed as a
** result of this change.
** 092 ECF 20090204 @41257 Improved getQuery() performance. We now cache
** Query objects more aggressively, which avoids
** the overhead of Hibernate's initialization of
** these objects when possible.
** 093 ECF 20090212 @41295 Added bulk eviction method. Exposed Hibernate
** session via bind() method.
** 094 ECF 20090311 @41502 Added support for limit/max value inlining in
** a SELECT statement. We do this only in very
** limited cases: currently only for simple load
** queries.
** 095 ECF 20090601 @42576 API change. Renamed load(RecordIdentifier) to
** quickLoad(RecordIdentifier). Also made method
** public, as it is needed from dirty package.
** 096 ECF 20090619 @42771 Added scroll() variant which accepts ScrollMode.
** Existing variant now calls this variant with a
** default ScrollMode of SCROLL_INSENSITIVE.
** 097 CA 20090625 @42945 Transaction roll back is executed at database
** level, so session needs to be flushed before the
** actual roll back.
** 098 ECF 20090702 @43036 Fixed session closing/cleanup logic. Removed
** deprecated code.
** 099 ECF 20090711 @43188 Implemented implicit transaction support. To
** prevent Hibernate from beginning open-ended
** transactions for query operations, we now open an
** implicit transaction for each such operation.
** These are closed either when an explicit
** transaction is opened, or after an idle timeout.
** 100 ECF 20090716 @43219 Fixed regression in #099 (@43188). In the event a
** session was marked as broken in the implicit
** transaction timer thread, this state was never
** being reset after a new session has been opened.
** 101 ECF 20090719 @43305 Added safety code to implicit commit task. It is
** more resilient to unexpected errors (such as
** OOME), to avoid taking down the timer thread.
** 102 ECF 20090722 @43325 Disabled implicit transaction commit thread. It
** is the prime suspect in a "possible nonthreadsafe
** access to session" error. May be re-enabled at a
** later time.
** 103 ECF 20090723 @43339 Made preprocessQueryParameter() public. This
** static method is needed by the dirty subpackage.
** 104 CA 20090724 @43381 Rolled back H091 (#41239) - removed the
** buffer.isStale() dependency in
** Context.getSession().
** 105 ECF 20090723 @43418 Rewrote implicit transaction support. Removed
** secondary thread and push/pop calls from low
** level query services. These must be implemented
** by callers instead to enable coarser, implicit
** transaction granularity (i.e., fewer transactions
** when possible). If a session is being used only
** for an implicit transaction, it is made read-only
** to prevent unnecessary session flushing.
** 106 ECF 20090810 @43587 Fixed SessionListener notifications. Modified
** notification mechanism to allow more event types.
** Notify listeners when a transaction is about to
** be committed, in addition to session closing
** events.
** 107 ECF 20090811 @43599 Fixed regressions in #105 (@43418). Implicit
** transaction use count was not being properly set
** and used in some places. Fixed endTransaction(),
** which was nulling out active transaction too
** early.
** 108 CA 20090901 @43818 In initializeInstance, catch any StopCondition's
** and re-throw them (do not pack it in a generic
** runtime exception).
** 109 ECF 20090925 @44029 Temporarily disabled identity pool. There is a
** defect in the implementation of IdentityPool that
** can prevent primary keys from being distributed.
** When that is fixed, this should be re-enabled.
** Affected code was commented out from
** createIdentityManager().
** 110 CA 20091001 @44062 Modified executeSQLQuery() to leave the statement
** open; else, it will not be able to iterate
** through the returned result set.
** 111 ECF 20091002 @44082 Re-enabled identity pool. Also made additional
** change to executeSQLQuery() to require the caller
** to push/pop the implicit transaction counter. We
** cannot do this in the method, as this could cause
** the transaction to be closed before the caller
** could process results.
** 112 SVL 20091019 @44145 Added support for the trigger manager.
** 113 SVL 20091126 @44442 Added quickLoad(RecordIdentifier, boolean)
** function.
** 114 CA 20100115 @44527 Added executeSQL and executeSQLQuery versions
** which accept query parameters.
** 115 CA 20101028 All metadata string query parameters and results
** will be processed using dialect-specific case.
** Do not prepare permanent H2 database.
** 116 CA 20101202 The permanent H2 database needs to be prepared
** in a special way - the custom functions need to
** be registered with the HQL preprocessor.
** 117 AIL 20121206 Updated from character to TextOps.
** 118 SVL 20120331 Upgraded to Hibernate 4.
** 119 CA 20130620 Added APIs to check if a given class is one of P2J's Hibernate
** compatible types and to disambiguate a BDT or Java native type to a
** Hibernate compatible type.
** 120 OM 20130531 Added preprocessing of datetime/-tz parameters.
** Added small patch for binding composed-type parameters.
** Maybe this should be used for all parameters when switching
** from positional to named parameters
** Do not close sql connection when 4GL error occurs in CAN-FINDs.
** 121 VMN 20130830 Added lockTable method.
** 122 OM 20130829 Enriched the parameter list of load() methods with a boolean for
** specifying if the record should be forced to be read from database.
** Added support for -rereadnolock P4GL startup parameter emulation.
** 123 ECF 20130804 Added base metadata support.
** 124 ECF 20131028 Added lock metadata support.
** 125 CA 20131013 Added no-op deleted() method, required by the changes in Finalizable
** interface.
** 126 OM 20131206 Added initialization call for DatabaseTriggerManager.
** 127 SVL 20140210 Set JPA-style HQL parameters instead of positional.
** 128 GES 20140206 Added isActive() to allow callers to determine if the persistence
** layer is or is not disabled.
** 129 ECF 20140312 Replaced Apache commons logging with J2SE standard logging.
** 130 ECF 20140318 Minor memory optimization when gathering JDBC index metadata.
** 131 CA 20140513 Added a weight for the context-local var, to ensure predetermined
** order during context reset.
** 132 ECF 20140624 Rolled back 119. See persist.type.TypeHelper.
** 133 OM 20140711 Changed initialization location for database triggers to pkgroot/dmo/.
** 134 ECF 20140930 Wrap IllegalStateException in PersistenceException in rollback, to
** be consistent with the declared API and to be less catastrophic for
** callers.
** 135 ECF 20141204 Fixes to transaction and context cleanup.
** 136 OM 20150112 Replaced calls to deprecated method. Additional code cleanup.
** 137 ECF 20150221 Raise STOP condition instead of ERROR for JDBC-level errors, to avoid
** getting caught in RETRY loops for unrecoverable errors.
** 138 ECF 20150326 Optionally log diagnostic information on begin transaction conflicts.
** 139 ECF 20150504 Enable explicit deregistration of session listeners.
** 140 ECF 20150811 Fixed database connection leak during context cleanup. Added some
** debug logging and adjusted level of existing log statements.
** 141 ECF 20150821 Minor format fix and import change.
** 142 ECF 20150906 Change code to match BufferManager API changes; override hashCode
** and equals implementations.
** 143 ECF 20150928 Reimplemented query cache to reduce memory use. Refactored certain
** methods used frequently within the persist package to reduce
** unnecessary context local lookups.
** 144 OM 20151028 Added comment/javadoc.
** 145 OM 20140721 Called preparePermanentDatabase() for MS SQL database server in order
** to prepare HQL for support UDFs from loaded assemblies.
** 146 ECF 20160305 Collect index metadata at initialization in multiple threads to
** reduce server initialization time.
** 147 ECF 20160429 Do not make queries against _temp database cacheable. Reimplemented
** executeSQL variants to work directly with JDBC, rather than using
** Hibernate's SQLQuery, which was too expensive. Fixed load method
** variants to not rely upon DMO reference counting in their refresh
** logic. Refined rereadnolock implementation, at least when used by
** RandomAccessQuery.
** 148 ECF 20160512 Fixed deregisterSessionListener.
** 149 ECF 20160610 Optimized getSingleSQLResult.
** 150 OM 20160603 Fixed rtrimming to space character only.
** 151 IAS 20160621 Added support for read-only tables
** 152 ECF 20160614 Reduce unnecessary flushing. Expanded use of read-only metadata.
** Further refined rereadnolock implementation.
** 153 IAS 20160701 Fixed [3166] error message.
** 154 IAS 20160714 Use legacy name instead of DMO alias.
** 155 OM 20160715 Double guarded saving of template records to database. Prevented
** ContextLocalCleanupException to be thrown when client disconnects.
** 156 ECF 20160820 Fixes to support better concurrency and better detect records
** deleted in other sessions. Includes new lock timeout feature and
** better integration with record lock context.
** 157 OM 20160905 Small optimizations.
** 158 ECF 20160831 Performance enhancements. Improved use of read-only table
** information. Reworked session flushing to allow better batching of
** database statements. Initialize P2OLookup instances at server
** startup. Logging changes.
** 159 ECF 20160929 Removed obsolete enableFlush feature. Use implicit transactions
** where possible to reduce flushing.
** 160 HC 20171003 Moved the "rereadnolock" check in the introduced class LegacyCLI.
** 161 ECF 20171116 Treat NonUniqueObjectException during new Session setup as non-fatal.
** 162 OM 20180108 Fixed FWD Version printed for database.
** 163 ECF 20180219 Reduce unnecessary session flushing.
** 164 ECF 20180418 Added logging of unrecoverable exception to handleException.
** 165 SBI 20180518 Renamed file-system/pkgroot into legacy-system/pkgroot.
** 166 ECF 20180609 Further reduce unnecessary session flushing.
** 167 ECF 20180809 Ensure template record is not attached to session by a lock request.
** 168 OM 20190117 Basic support for VST metadata.
** ECF 20190325 Minor code de-duplication.
** HC 20190401 Fixed a H2 DB performance issue during initialization, caching of column data
** types didn't work correctly for H2.
** 169 OM 20190624 Allowed initialization of DataSetManager.
** 170 AIL 20190722 Removed initialization of DatabaseTriggerManager.
** 171 CA 20190902 Fixed problems when connecting to a FWD server running a database
** schema which is not actively managed by the requester, too.
** 172 ECF 20200419 New ORM implementation. Removed Hibernate dependencies.
** 173 GES 20200804 Removed ErrorManager context-local usage.
** ECF 20200909 Added handling for stale records when creating a database session.
** OM 20200919 Improved access to dmo metadata.
** ECF 20200919 Eliminate use of Session.refresh where no longer necessary.
** AIL 20200919 Initialize TemporaryDatabaseManager.
** ECF 20201026 Increase size of query LRU cache.
** OM 20201120 Avoid UniqueResultException reaching client UI.
** IAS 20201125 Do not re-use re-written queries
** OM 20210223 Invalidate index-based cache in the event of low-level SQL operations.
** ECF 20210911 Minor change to session use counter.
** ECF 20210924 Ensure a call to save(Record, Long) updates the internal DMO state.
** ECF 20210927 Minor cleanup.
** IAS 20211223 Special processing of errors raised by UDFs.
** IAS 20220221 Call a correct version of the getFWDVersion (Java or SQL).
** IAS 20220324 Re-worked LockTableUpdater.
** OM 20220516 Added collation support for _meta databases (use the H2 dialect).
** IAS 20220608 Provide additional information in reportUDFVersion().
** IAS 20220712 Create missing UDFs
** IAS 20220816 Do not use 'udf' schema for non-PostgreSQL UDFs
** SBI 20220921 Added save(Record) to update the existing record.
** CA 20220901 Refactored scope notification support: ScopeableFactory was removed, and the
** registration is now specific to each type of scopeable. For each case, the block
** will be registered for scope support (for that particular scopeable) only when
** the scopeable is 'active' (i.e. unnamed streams or accumulators are used). This
** allows a lazy registration of scopeables, to avoid the unnecessary overhead of
** processing all the scopeables for each and every block.
** TJD 20220504 Upgrade do Java 11 minor changes
** CA 20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded
** as 'partial/incomplete'.
** RAA 20221221 Modified the execution of sql's. They now pass through the SQLStatementLogger
** in case logging is intended.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** CA 20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded
** as 'partial/incomplete'.
** 174 RAA 20230213 Added the possibility to create a query defined with lazy.
** RAA 20230208 Removed the checks for no-undo as they are no longer necessary.
** AL2 20230224 Check at persistence level is lazy is supported.
** 175 IAS 20230405 Added getQuery(fql) method.
** Fixed second pass query generation for queries with CONTAINS operator
** Added DATE-RELATION to the 'scroll', 'list', an 'getQuery' parameters
** 176 IAS 20230410 Added 'old' methods w/o DataRelation argument.
** 177 SR 20230510 Removed readOnly parameter and fixed getQuery new signature.
** 178 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 179 RAA 20230518 Replaced lambdas with method referencing when using SQLStatementLogger.
** 180 RAA 20230607 SQLs are now executed through SQLExecutor instead of SQLStatementLogger.
** 181 RAA 20230615 Added SQL as a parameter when using SQLExecutor.
** 182 DDF 20230627 Made the size of the Context.staticQueryCache configurable.
** DDF 20230627 Removed the static block that reads the configuration sizes of the cache.
** CacheManager will handle the finding of the configuration size and the
** cache creation.
** DDF 20230627 Initialize server bootstrap configurations.
** DDF 20230627 Made the Context cache final.
** DDF 20230628 Initialize CacheManager before DatabaseManager.
** DDF 20230705 Initialize P2JQueryExecutor and Validation configurations.
** DDF 20230706 Replaced createLRUCache call with another one that will use a null
** discriminator by default.
** 183 DDF 20230703 Manage reclaimable sessions using SessionFactory.
** DDF 20230703 Initialize the SessionCloseThread which is tasked with closing unused sessions
** across all SessionFactory instances.
** DDF 20230703 Use a custom Consumer to manage association of records with new sessions.
** DDF 20230703 Add missing assignment of session in lambda.
** 184 CA 20230728 Ensure the current JDBC connection is not leaked at context cleanup.
** ECF 20230807 Do not let getSingleSQLResult create a long-lived session.
** DDF 20230807 Directly close the session when reclaiming is disabled.
** 185 ECF 20230808 Fixed regression in getSingleSQLResult method.
** 186 TT 20230621 Replaced LegacyCLI.getInstance() with SessionUtils._startupParameters()
** 187 AL2 20230822 Removed checkReadOnly method.
** 188 RAA 20230724 Replaced FunctionWithException with QueryStatement.
** RAA 20231006 Removed scrollThreshold, listThreshold and other fields used for logging.
** 189 OM 20231218 Added implementation for DBCODEPAGE and DBCOLLATION 4GL functions.
** 190 OM 20231214 Converted list() method to optionally return a list of values instead of
** attempting to hydrate and return them as record(s).
** 191 OM 20240131 Fixed flaw in DBCODEPAGE / DBCOLLATION implementation.
** 192 DDF 20240207 Notify TRANSACTION_COMMITTING event when committing a database transaction.
** 193 RAA 20230802 Added supportsValidateMode() function.
** RAA 20230802 Changed name of validateMode to allowDBUniqueCheck.
** RAA 20231103 Added allowDBUniqueCheck() function.
** RAA 20231127 Swapped condition places in allowDBUniqueCheck.
** RAA 20240316 Rebase fix: added allowDBUniqueCheck flag.
** 194 DDF 20240322 Only close the open temporary database when cleaning up the context.
** 195 AL2 20240328 Added lazyMode to the query key (used for caching).
** 196 DDF 20240404 Removed change for closing the temporary database when cleaning up the
** context.
** 197 OM 20240520 Fixed connection management in executeSQL().
** 198 RAA 20240423 Added multi-tenancy support.
** RAA 20240426 The Persistence.Context is now registered as a TenantChangeListener.
** RAA 20240513 When registering the context as a listener, provide the name of the database.
** Added Context.getSession() function with a parameter intended for multi-tenancy.
** RAA 20240613 Added tenantId field in the persistence context.
** BS 20230512 Shared cache instance per persistent database.
** RAA 20240627 Made the RecordNursery tenant-aware. Enabled back the session reclaimable
** feature when in multi-tenancy mode.
** RAA 20240702 When a tenant changes inside the context, expire its reclaimable session.
** RAA 20240710 Allow the database and the dialect to change depending on the tenant
** holding the context.
** RAA 20240715 Clarified the type of database name occurrences.
** RAA 20240717 Removed landlord database field and adjusted getSession.
** RAA 20240726 Moved TenantConfig.initialize() call. Adjusted parameters for setTenantDatabase.
** RAA 20240730 If the database used in the Context constructor does not have a logical name,
** compute it.
** RAA 20240807 If the database name is null when retrieving a session, make no tenant changes.
** RAA 20240808 Added FastFindCache and Database fields in the context.
** DDF 20240808 Added getDatabase() method for Context.
** 199 CA 20240723 Fixed a possible NPE when a the cause of a PersistenceException is checked
** in 'load'.
** 200 SB 20240828 Added the unsafe flag which prevents an immediately rollback on a failed SQL
** query. Refactored exception handling depending. Refs #8968.
** 201 OM 20240813 Invalidate all buffers to avoid leaking from one tenant database to another.
** 20240909 Concurrent multitenancy functionality implementation.
** 202 OM 20240913 Made RecordLockContext context private instead of shared.
** 203 TJD 20240918 Defer tenant initialization for remote databases
** 204 DDF 20241002 Only close the open temporary database when cleaning up the context (removed
** previously).
** 205 OM 20241001 Incremental support for tenants.
** 206 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 207 OM 20241211 RecordLockContext knowns its context from the creation time.
** 208 OM 20250110 Added sequence support for multi-tenant databases.
** 20250114 Fixed tenant re-authentication within the same context.
** 209 CA 20250122 Avoid creating new character instances in 'preprocessQueryParameter'.
** 210 AS 20250203 Reduced the logging level for unique find violations.
** 211 ICP 20250213 Removed the initialization of the CacheManager, it was moved into a server hook.
** 212 RNC 20250224 If the SQLException is recoverable, do not throw StopConditionException.
** 213 CA 20250303 Added 'invalidateCache', an API to invalidate the caches and potentially lock
** exclusively all records specified by an SQL.
** Added 'refreshBuffers', an API to force reload of all buffers in current context,
** for the specified DMO.
** 214 OM 20250211 More verbose error in initializeTenant(). New TenantManager.getTenant() param.
** 215 OM 20250315 Added support for readonly meta tables.
** 216 ES 20250409 Added timeout parameter.
** 217 OM 20250521 Fixed UDF type detection.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.sql.*;
import java.util.*;
import java.util.BitSet;
import java.util.Date;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.cache.*;
import com.goldencode.p2j.persist.deploy.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.dirty.DirtyShareSupport;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.hql.*;
import com.goldencode.p2j.persist.id.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.SQLExecutor.QueryStatement;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.ErrorManager.*;
import com.goldencode.p2j.util.logging.*;
/**
* The central point for fundamental persistence services in the P2J environment. This implementation is
* backed by the FWD ORM framework. This class provides basic CRUD (create, read, update, delete) services,
* as well as methods to begin/commit/rollback database-level transactions.
* <p>
* An instance of this class is retrieved from {@link PersistenceFactory#getInstance(Database)} method
* One instance of this class is associated with a single, physical database. Multiple user sessions can
* use this shared instance; each will have its own context.
* <p>
* An instance of a concrete {@link com.goldencode.p2j.persist.lock.LockManager} implementation is created
* for each instance of this class. The implementation to use is queried from the directory service,
* and defaults to the {@link InMemoryLockManager} if no other implementation is specified in the
* directory.
* <p>
* Transactions are explicitly managed with {@link #beginTransaction}, {@link #commit} and {@link #rollback}.
* Only one transaction per client context may be active at a time. These are database-level transactions,
* which may or may not coincide with application-level transactions.
* <p>
* <strong>External code which directly accesses query methods within the {@code Persistence} class should
* always properly bracket its use of these methods with transactions</strong>.
* <p>
* ORM sessions are managed implicitly, transparently to application code and even to other classes within
* the persistence framework. No more than one session per user context may be active at a time. Sessions are
* opened lazily as they are needed to fulfill database operations: executing a query, persisting a record,
* creating a temporary table, etc. They are closed when the runtime environment detects that they are no
* longer needed by any active resource. Upon opening a new session, the current context's {@link
* BufferManager} instance is interrogated for all active records. These are the current records stored in
* all open record buffers for the database matching the current {@code Persistence} instance. They represent
* detached records previously associated with the last open session (if any).
*/
public class Persistence
implements UnsafePersistence,
TenantChangeListener
{
/** Well-known name of ID generator sequence */
public static final String ID_GEN_SEQUENCE = "p2j_id_generator_sequence";
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(Persistence.class.getName());
/** Default lock manager implementation */
private static final String DEFAULT_LOCK_MANAGER =
"com.goldencode.p2j.persist.lock.InMemoryLockManager";
/** Default identity manager implementation */
private static final String DEFAULT_IDENTITY_MANAGER =
"com.goldencode.p2j.persist.id.SequenceIdentityManager";
/** Special values for a {@code codepage} or {@code collation} which has not been detected yet. */
private static final String NOT_DETECTED_YET = "NOT_DETECTED_YET";
/** Flag indicating whether records are joined using foreign keys */
private static final boolean foreignKeysEnabled =
Utils.getDirectoryNodeBoolean(null, "persistence/foreign-keys", false, false);
/** <code>true</code> if the persistence layer can be used. */
private static volatile boolean active = false;
/** The offset in the {@code contexts} array of the shared/common/default instance. */
public static final int SHARED_IDX = 0;
/**
* The offset in the {@code contexts} array of the private instance. The object at this position may be
* {@code null} for non-tenant enabled applications or if no tenant was authenticated.
*/
public static final int PRIVATE_IDX = 1;
/**
* A constant used for clearer meaning that the shared/common/default context is preferred. This will be
* the cases for temporary and meta databases since for these tables, the {@code DmoMeta.multiTenant} is
* always {@code false} (ie shared, not tenant specific).
* <p>
* When this is passed to a method, the {@code SHARED_IDX} context is used.
*/
public static final boolean SHARED_CTX = true;
public static final boolean META_CTX = SHARED_CTX;
public static final boolean TEMP_CTX = SHARED_CTX;
/**
* A constant used for clearer meaning that the private context is preferred. This value is used when
* {@code DmoMeta.multiTenant}.
* <p>
* In case the tenant was not authenticated and the private context was not created, the shared context is
* used as default.
* <p>
* When this is passed to a method, the {@code PRIVATE_IDX} context is used.
*/
public static final boolean PRIVATE_CTX = false;
/** Context local object to manage persistence variables. */
private final ContextLocal<Context[]> contexts = new ContextLocal<Context[]>()
{
@Override
public WeightFactor getWeight() { return WeightFactor.LEVEL_1; }
/**
* Creates the default array of contexts. The default/shared/common one is automatically created. The
* shared context will be created later, when and if a tenant authenticate for this logical database.
*
* @return The initial value for the array of contexts.
*/
@Override
protected Context[] initialValue()
{
return new Context[] { new Context(defaultTenant), null };
}
/**
* Cleans up the contexts. The shared context is cleaned by default, the private one only if a tenant
* vas ever authenticated.
*
* @param value
* The last value this context local variable contained.
*/
@Override
protected void cleanup(Context[] value)
{
value[SHARED_IDX].cleanup();
if (value[PRIVATE_IDX] != null)
{
value[PRIVATE_IDX].cleanup();
}
}
};
/** Flag that determines if multi-tenancy is GLOBALLY enabled for this database or not. */
private final boolean isMultiTenancyEnabled;
/** Database dialect helper. */
private final Dialect dialect;
/** The CP for this database. */
private String codepage = NOT_DETECTED_YET;
/** The collation for this database. */
private String collation = NOT_DETECTED_YET;
/** Does this object represent the temporary database? */
private final boolean temporary;
/** Flag that marks if server side index validation can be skipped or not. */
private final boolean allowDBUniqueCheck;
/** Cached hash code */
private final int hashCode;
/** Lock Timeout in seconds */
private final long timeout;
/** The default tenant data. */
protected final Tenant defaultTenant;
/**
* The map with all tenants which share the database of this {@code Peristence} object, mapped by their
* name.
*/
private final ConcurrentHashMap<String, Tenant> tenantData = new ConcurrentHashMap<>();
/**
* A container class which stores tenant-specific objects. Although the persistence tracks all these
* {@code Tenant}, at any given moment, only one of is active in each user context.
*/
protected static class Tenant
{
/** The name of the tenant that was assigned to this context, if any. */
private final String tenantName;
/** The id of the tenant that was assigned to this context */
private final int tenantId;
/** Database served by this object. */
private final Database database;
/** DMO version tracker (persistent databases only). */
private final DmoVersioning dmoVersion;
/** Pessimistic lock manager */
private final AssignOnceWrapper<LockManager<String>> lockManager = new AssignOnceWrapper<>();
/** Object which keeps lock metadata in sync with record lock activity. */
private LockTableUpdater lockTableUpdater = null;
/** Identity manager. */
private final AssignOnceWrapper<IdentityManager> identityManager = new AssignOnceWrapper<>();
/** Context-local record lock context */
private final ContextLocal<RecordLockContext> lockContext = new ContextLocal<>();
/**
* Constructs a {@code Tenant} starting with its name and the database it serves. Not all 'final' fields
* will be initialized now, some of them use {@code AssignOnceWrapper} for a lazy initialization.
*
* @param database
* The physical database for which the {@code Tenant} is constructed.
* @param tenantId
* The tenant's id.
* @param tenantName
* The tenant name.
*/
public Tenant(Database database, int tenantId, String tenantName)
{
this.tenantName = tenantName;
this.tenantId = tenantId;
this.database = database;
this.dmoVersion = database.isTemporary() ? null : new DmoVersioning();
}
/**
* Returns a short description of this object, usually needed while debugging.
*
* @return a {@code String} which identifies the physical database around which this object is built.
*/
@Override
public String toString()
{
return "Tenant{" + database + '}';
}
}
/**
* Construct an instance for a particular, physical database, storing the dialect which is used to create
* a portability layer.
*
* @param database
* Database with which this instance is permanently associated.
* @param dialect
* Portability object used to issue dialect-specific commands to the database.
*/
protected Persistence(Database database, Dialect dialect)
{
this.hashCode = super.hashCode();
this.temporary = database.isTemporary();
this.dialect = dialect;
this.allowDBUniqueCheck = temporary && dialect.allowDBUniqueCheck();
this.isMultiTenancyEnabled = !temporary && TenantConfig.isEnabled() && !database.isMeta();
this.defaultTenant = new Tenant(database,
TenantManager.DEFAULT_TENANT_ID,
TenantManager.DEFAULT_TENANT_NAME); // default tenant always available
this.tenantData.put(TenantManager.DEFAULT_TENANT_NAME, defaultTenant);
// for remote databases tenant initialization is deferred for after session is created
if (database.isLocal())
{
initializeTenant(defaultTenant);
}
this.timeout = ConnectionManager.getTimeOut(getDatabase().getName());
}
/**
* Get the instance of this class associated with the given, physical
* database name, creating it first if necessary. Newly created instances
* are stored in a static cache.
*
* @param name
* Name of physical database with which the instance is
* permanently associated.
*
* @return Persistence object associated with the given database.
*
* @deprecated Use {@link PersistenceFactory#getInstance(String)} instead.
*/
@Deprecated
public static Persistence get(String name)
{
return PersistenceFactory.getInstance(name);
}
/**
* Initialize various infrastructure components of the persistence framework:
* <ul>
* <li>{@code DataTypeHelper}, which is used when preprocessing FQL where clauses.
* <li>{@code DatabaseManager}, which configures the ORM environment for the databases
* to which the framework connects.
* <li>{@code P2OLookup}, instances of which are used for runtime, on-the-fly query and
* temp-table conversion.
* <li>{@code DatabaseTriggerManager}, which tracks and fires database triggers at the
* appropriate times.
* </ul>
*
* @throws IllegalStateException
* if invoked more than once.
* @throws PersistenceException
* if any error is encountered initializing the persistence framework.
*
* @see DataTypeHelper#initialize()
* @see DatabaseManager#initialize()
* @see TemporaryDatabaseManager#initialize()
* @see P2OLookup#initialize()
*/
public static void initialize()
throws PersistenceException
{
Session.bootstrap();
DataTypeHelper.initialize();
TemporaryDatabaseManager.initialize();
TenantConfig.enableMultiTenancy();
DatabaseManager.initialize();
Validation.bootstrap();
DirtyShareSupport.bootstrap();
SessionFactory.SessionCloseThread.initialize();
// if runtime conversion services are required, initialize schema lookup resources
if (ConversionPool.isInitialized())
{
try
{
P2OLookup.initialize();
}
catch (SchemaException exc)
{
throw new PersistenceException(exc);
}
}
TenantConfig.initialize();
// this is volatile and only assigned here, so it should be safe without synchronization
active = true;
if (LOG.isLoggable(Level.INFO))
{
LOG.info("Persistence services ready");
}
}
/**
* Reports if the persistence layer can be used or not.
*
* @return <code>true</code> if the persistence layer is initialized and can be used.
*/
public static boolean isActive()
{
return active;
}
/**
* Perform special handling on a query substitution parameter to avoid
* errors during query execution. This entails:
* <ul>
* <li>Trimming all trailing spaces, tabs, line feeds, and carriage
* returns from any parameter of type <code>character</code>. Any
* parameter affected is replaced with a new <code>character</code>
* instance at the same index position within <code>args</code>.
* <li>Clipping out of bounds date values to the range supported by the
* backing database's implementation of a date data type. Any
* parameter affected is replaced with a new <code>date</code>
* instance at the same index position within <code>args</code>.
* </ul>
* <p>
* Note: this method has special processing for <code>null</code> bytes
* embedded in <code>character</code> variables. The text of the variable
* is truncated starting at the first embedded <code>null</code> byte.
* This means that the resulting substitution parameters are guaranteed
* to never include <code>null</code> bytes.
*
* @param arg
* A single substitution parameter.
* @param dialect
* Database dialect.
*
* @return The given parameter, processed as described above.
*/
public static Object preprocessQueryParameter(Object arg, Dialect dialect)
{
Object processed = arg;
// Trim text arguments.
if (arg instanceof character)
{
character c = (character) arg;
if (!c.isUnknown())
{
// this obtains a clone of the character variable, truncating it's text at any null byte that may
// be embedded
c = c.isJavaSpacifyNull() ? c.safeDuplicate() : c;
// this is safe because it doesn't alter the original reference's state but rather replaces that
// reference
processed = c.getValue().endsWith(" ") ? TextOps.rightTrim(c, " ") : c;
}
}
// "Clip" dates which are out of range. This may result in
// incorrect query behavior (hence the logged warning), but will
// avoid catastrophic failure.
else if (arg instanceof date)
{
date d = (date) arg;
Date when = d.dateValue(); // TODO: what if [d] is unknown (so [when] is null)?
Date low = dialect.getLowDate();
if (low != null && when != null && when.before(low))
{
if (d instanceof datetimetz)
{
datetimetz dtz = (datetimetz)d;
processed = new datetimetz(new datetime(low), dtz.getOffset());
}
else if (d instanceof datetime)
{
processed = new datetime(low);
}
else
{
processed = new date(low);
}
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
"Query substitution date parameter " + d.toStringMessage() +
" was reset to the earliest date (" + low +
") supported by the backing database");
}
}
Date high = dialect.getHighDate();
if (high != null && when != null && when.after(high))
{
if (d instanceof datetimetz)
{
datetimetz dtz = (datetimetz)d;
processed = new datetimetz(new datetime(high), dtz.getOffset());
}
else if (d instanceof datetime)
{
processed = new datetime(high);
}
else
{
processed = new date(high);
}
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE,
"Query substitution date parameter " + d.toStringMessage() +
" was reset to the latest date (" + high +
") supported by the backing database");
}
}
}
return processed;
}
/**
* Determine whether records are joined using foreign keys.
*
* @return <code>true</code> if records are joined using foreign keys;
* <code>false</code> if records are joined using legacy key
* fields.
*/
static boolean isForeignKeysEnabled()
{
return foreignKeysEnabled;
}
/**
* Logs the FWD version returned by the getFWDVersion user-defined function (UDF/p2jpl) for
* this database. When this database is a temporary or not primary this method is a no-op.
* If the method fails to retrieve the version string, "undefined" version is reported in
* the log output.
*
* @param database
* The database in use.
* @param cfg
* The database configuration in use.
*
* @throws PersistenceException
* if there is a problem creating a database session or statement.
*/
void reportUDFVersion(Database database, DatabaseConfig cfg)
throws PersistenceException
{
String udfVersion = "undefined";
boolean useSQLUdfs;
String searchPath = "unknown";
try (Session session = new Session(database, cfg);
Connection conn = session.getConnection())
{
String sqlFunc = FQLPreprocessor.getRegisteredFunction(database, "getFWDVersion", null);
if (sqlFunc == null)
{
sqlFunc = "getFWDVersion"; // the function is not overloaded for all dialects
}
if (cfg == null)
{
cfg = DatabaseManager.getConfiguration(database);
}
useSQLUdfs = dialect.isNativeUDFsSupported() && !cfg.isUseJavaUDFs();
if (useSQLUdfs)
{
ScriptRunner.createMissingUdfs(database);
if ((DatabaseManager.getDialect(database) instanceof P2JPostgreSQLDialect) &&
!sqlFunc.startsWith("udf."))
{
sqlFunc = "udf." + sqlFunc;
}
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT " + sqlFunc + "()"))
{
if (rs.next())
{
String val = rs.getString(1);
if (val != null)
{
udfVersion = val;
}
}
}
catch (SQLException exc)
{
// do not propagate the exception, in case the UDF is not defined we want to
// report "undefined" version
LOG.log(Level.FINEST, "Error querying UDF version for " + database, exc);
}
if (dialect instanceof P2JPostgreSQLDialect)
{
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(
"SELECT setting FROM pg_settings WHERE name='search_path'"))
{
if (rs.next())
{
String val = rs.getString(1);
if (val != null)
{
searchPath = val;
}
}
}
catch (SQLException exc)
{
LOG.log(Level.FINEST, "Error querying search_path for " + database, exc);
}
}
}
catch (SQLException ex)
{
DBUtils.handleException(database, ex);
throw new PersistenceException("Error querying UDF version for " + database, ex);
}
LOG.info(String.format("UDF version for db %s is %s; UDFs style: %s; search_path: [%s]",
database,
udfVersion,
useSQLUdfs ? "SQL" : "Java",
searchPath
)
);
}
/**
* Override the parent's implementation to return a hash code cached at construction.
*/
@Override
public final int hashCode()
{
return hashCode;
}
/**
* Override the parent's implementation to ensure it is consistent with {@link #hashCode}.
*/
@Override
public final boolean equals(Object o)
{
return super.equals(o);
}
/**
* Invalidate all caches related to this DMO.
*
* @param dmoIface
* The DMO which requires cache invalidation.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public void invalidateCache(Class<? extends DataModelObject> dmoIface)
throws PersistenceException
{
invalidateCache(dmoIface, null, null);
}
/**
* Invalidate all caches related to this DMO.
* <p>
* If a SQL like <code>select recid from ... where ...</code> is provided, then an
* <code>EXCLUSIVE-LOCK</code> will be attempted on all records.
*
* @param dmoIface
* The DMO which requires cache invalidation.
* @param lockSql
* The SQL to get the PKs for the records to be locked exclusively.
* @param args
* Any arguments for the SQL, or <code>null</code> otherwise.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public void invalidateCache(Class<? extends DataModelObject> dmoIface, String lockSql, Object[] args)
throws PersistenceException
{
Session session = getSession();
DmoMeta dmoInfo = DmoMetadataManager.registerDmo(dmoIface);
String dmoSqlTable = dmoInfo.getSqlTableName();
String dmoImplName = dmoInfo.getImplementationClass().getName();
session.invalidateRecords(dmoImplName, null);
FastFindCache ffCache = FastFindCache.getInstance(dmoInfo.isTempTable(),
getDatabase(!dmoInfo.multiTenant));
ffCache.invalidate(dmoInfo.getId(), null);
DmoVersioning versioning = defaultTenant.dmoVersion;
if (lockSql != null)
{
LockManager<String> lm = getLockManager();
ScrollableResults<Long> res = executeSQLQuery(lockSql, args);
while (res.next())
{
Long pk = (Long) res.get(true)[0];
lm.lock(LockType.EXCLUSIVE, new RecordIdentifier<>(dmoSqlTable, pk), true);
versioning.increment(new RecordIdentifier<>(dmoImplName, pk));
}
}
else
{
versioning.increment(dmoImplName);
}
}
/**
* Notify all buffers in the current FWD context, that they need to do a 'force refresh', as there have
* been changes via direct JDBC access.
*
* @param dmoIface
* The DMO which requires notifications.
*
* @throws PersistenceException
* if any error occurs refreshing the buffers.
*/
@Override
public void refreshBuffers(Class<? extends DataModelObject> dmoIface)
throws PersistenceException
{
handle hbuff = new handle();
try
{
DmoMeta dmoInfo = DmoMetadataManager.registerDmo(dmoIface);
// we need a buffer to use as 'reference' here.
// Create a temporary one and delete it immediately after
RecordBuffer.create(hbuff, dmoInfo.legacyTable);
ChangeBroker changeBroker = ChangeBroker.get();
BufferImpl buff = (BufferImpl) hbuff.get();
RecordBuffer rb = buff.buffer();
changeBroker.stateChanged(RecordChangeEvent.Type.BUFFER_REFRESH, rb, null, null, null);
}
finally
{
if (hbuff._isValid())
{
HandleOps.delete(hbuff);
}
}
}
/**
* Get the database dialect helper object associated with the database
* backing this object.
*
* @return Database dialect helper.
*/
public Dialect getDialect()
{
return dialect;
}
/**
* Get the database lock time-out.
*
* @return See above.
*/
public long getTimeOut()
{
return timeout;
}
/**
* Get the database information object associated with this persistence
* service object. If the database cannot be retrieved from the context, take the main class object.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #rollback(boolean)} instead.
*
* @return Database information object.
*/
public Database getDatabase()
{
return defaultTenant.database;
}
/**
* Get the database information object associated with this persistence
* service object. If the database cannot be retrieved from the context, take the main class object.
*
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @return Database information object.
*/
public Database getDatabase(boolean sharedDb)
{
if (tenantData.size() == 1)
{
return defaultTenant.database;
}
return getContext(sharedDb).tenant.database;
}
/**
* Get the lock manager associated with this <code>Persistence</code>
* instance. For temp tables, this will be <code>null</code>, since temp
* table data is naturally compartmentalized and record locking is
* meaningless in this case. For permanent databases, this method will
* return the lock manager created when this class was instantiated.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #rollback(boolean)} instead.
*
* @return Lock manager instance associated with this persistence service object.
*/
public LockManager<String> getLockManager()
{
if (tenantData.size() == 1)
{
return defaultTenant.lockManager.get();
}
return getContext(PRIVATE_CTX).tenant.lockManager.get();
}
/**
* Get the lock manager associated with this <code>Persistence</code>
* instance. For temp tables, this will be <code>null</code>, since temp
* table data is naturally compartmentalized and record locking is
* meaningless in this case. For permanent databases, this method will
* return the lock manager created when this class was instantiated.
*
* @param shared
* Only used if a regular tenant is active.<br>
* If {@code true} the lock manager for default (shared/common) database is returned.
* Otherwise, the one of the tenant set on current context is returned.
*
* @return Lock manager instance associated with this persistence service object.
*/
public LockManager<String> getLockManager(boolean shared)
{
if (tenantData.size() == 1)
{
return defaultTenant.lockManager.get();
}
return getContext(shared).tenant.lockManager.get();
}
/**
* Get the identity manager associated with this <code>Persistence</code>
* instance. For temp tables, this will be <code>null</code>, since for
* temp tables unique primary keys generated using {@link
* TemporaryBuffer#nextPrimaryKey()}. For permanent databases, this
* method will return the identity manager created when this class was
* instantiated.
*
* @param shared
* Only used if a regular tenant is active.<br>
* If {@code true} the identity manager for default (shared/common) database is returned.
* Otherwise, the one of the tenant set on current context is returned.
*
* @return Identity manager instance associated with this persistence service object.
*/
public IdentityManager getIdentityManager(boolean shared)
{
if (tenantData.size() == 1)
{
return defaultTenant.identityManager.get();
}
return getContext(shared).tenant.identityManager.get();
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by
* the result set are not locked by this method. This method creates a new, context local
* ORM session if necessary.
* <p>
* <strong>Note:</strong><br>
* While this method attempts to use a server-side database cursor to fetch results, some JDBC
* driver implementations (e.g., PostgreSQL) may store an intermediate form of the
* <em>entire</em> result set in memory instead, effectively doubling the amount of memory
* required. To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
* instead. Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
* JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
* fetch smaller batches of results at a time. Consult your JDBC driver documentation and/or
* source code to determine whether this is an issue for your JDBC implementation.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql, Object[] values)
throws PersistenceException
{
return scroll(null, fql, values, 0, 0, null, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by
* the result set are not locked by this method. This method creates a new, context local
* ORM session if necessary.
* <p>
* <strong>Note:</strong><br>
* While this method attempts to use a server-side database cursor to fetch results, some JDBC
* driver implementations (e.g., PostgreSQL) may store an intermediate form of the
* <em>entire</em> result set in memory instead, effectively doubling the amount of memory
* required. To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
* instead. Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
* JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
* fetch smaller batches of results at a time. Consult your JDBC driver documentation and/or
* source code to determine whether this is an issue for your JDBC implementation.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql, Object[] values, DataRelation relation)
throws PersistenceException
{
return scroll(null, fql, values, 0, 0, relation, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by
* the result set are not locked by this method. This method creates a new, context local
* ORM session if necessary.
* <p>
* <strong>Note:</strong><br>
* While this method attempts to use a server-side database cursor to fetch results, some JDBC
* driver implementations (e.g., PostgreSQL) may store an intermediate form of the
* <em>entire</em> result set in memory instead, effectively doubling the amount of memory
* required. To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
* instead. Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
* JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
* fetch smaller batches of results at a time. Consult your JDBC driver documentation and/or
* source code to determine whether this is an issue for your JDBC implementation.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql, Object[] values, int maxResults, int startOffset)
throws PersistenceException
{
return scroll(null, fql, values, maxResults, startOffset, null, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by
* the result set are not locked by this method. This method creates a new, context local
* ORM session if necessary.
* <p>
* <strong>Note:</strong><br>
* While this method attempts to use a server-side database cursor to fetch results, some JDBC
* driver implementations (e.g., PostgreSQL) may store an intermediate form of the
* <em>entire</em> result set in memory instead, effectively doubling the amount of memory
* required. To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
* instead. Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
* JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
* fetch smaller batches of results at a time. Consult your JDBC driver documentation and/or
* source code to determine whether this is an issue for your JDBC implementation.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation)
throws PersistenceException
{
return scroll(null, fql, values, maxResults, startOffset, relation, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set.
* Records represented by the result set are not locked by this method.
* This method creates a new, context local ORM session if necessary.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an
* empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @return An object which provides access to a scrollable cursor on the
* result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql,
Object[] values,
int maxResults,
int startOffset,
int scrollMode)
throws PersistenceException
{
return scroll(null, fql, values, maxResults, startOffset, null, scrollMode, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set.
* Records represented by the result set are not locked by this method.
* This method creates a new, context local ORM session if necessary.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an
* empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @return An object which provides access to a scrollable cursor on the
* result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation,
int scrollMode)
throws PersistenceException
{
return scroll(null, fql, values, maxResults, startOffset, relation, scrollMode, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
* set are not locked by this method.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be parsed and executed by ORM framework.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is non-positive, no
* upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive, an offset
* of 0 is used by default.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
int scrollMode)
throws PersistenceException
{
return scroll(entities, fql, values, maxResults, startOffset, null, scrollMode, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
* set are not locked by this method.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be parsed and executed by ORM framework.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is non-positive, no
* upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive, an offset
* of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation,
int scrollMode)
throws PersistenceException
{
return scroll(entities, fql, values, maxResults, startOffset, relation, scrollMode, false);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
* set are not locked by this method.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be parsed and executed by ORM framework.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is non-positive, no
* upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive, an offset
* of 0 is used by default.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @param lazyMode
* Flag that indicates whether the query should be executed in a lazy manner.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
int scrollMode,
boolean lazyMode)
throws PersistenceException
{
return scroll(entities, fql, values, maxResults, startOffset, null, scrollMode, lazyMode);
}
/**
* Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
* set are not locked by this method.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be parsed and executed by ORM framework.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is non-positive, no
* upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive, an offset
* of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param scrollMode
* Scroll mode which should apply to the result set.
* @param lazyMode
* Flag that indicates whether the query should be executed in a lazy manner.
*
* @return An object which provides access to a scrollable cursor on the result set.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation,
int scrollMode,
boolean lazyMode)
throws PersistenceException
{
Context local = getContext(entities);
ScrollableResults<T> results = null;
try
{
Query query = getQuery(local, fql, maxResults, startOffset, relation, values, lazyMode);
results = query.scroll(local.getSession(), scrollMode);
}
catch (UdfException exc)
{
local.closeSession(true);
ErrorEntry ee = exc.getErrorEntry();
ErrorManager.recordOrShowError(
new int[] {ee.num},
new String[] {ee.text},
true,
ee.prefix,
false,
false,
false,
true);
handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
}
catch (PersistenceException exc)
{
handleException(local, exc, "error executing query [" + fql + "]");
}
return results;
}
/**
* Execute an FQL query and return the results as a list. If the query returns multiple results
* per row, each element in the list will be an array of {@code Object}s.
* <p>
* No locking is attempted.
* <p>
* <strong>Note:</strong><br>
* This method should be used only for queries that are expected to return a relatively small
* result set, since the entire result set is stored in the returned list. Additionally, some
* JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
* set in memory, effectively doubling the amount of memory required. To avoid this, consider
* using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
* Using that method with a
* {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
* value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
* results at a time. Consult your JDBC driver documentation and/or source code to determine
* whether this is an issue for your JDBC implementation.
* <p>
* This overloaded variant always attempts to hydrate and return the result as {@code Record}s.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
*
* @return A list of results, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> List<T> list(String fql, Object[] values, int maxResults, int startOffset)
throws PersistenceException
{
return list(null, fql, values, maxResults, startOffset, null, true);
}
/**
* Execute an FQL query and return the results as a list. If the query returns multiple results
* per row, each element in the list will be an array of {@code Object}s.
* <p>
* No locking is attempted.
* <p>
* <strong>Note:</strong><br>
* This method should be used only for queries that are expected to return a relatively small
* result set, since the entire result set is stored in the returned list. Additionally, some
* JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
* set in memory, effectively doubling the amount of memory required. To avoid this, consider
* using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
* Using that method with a
* {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
* value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
* results at a time. Consult your JDBC driver documentation and/or source code to determine
* whether this is an issue for your JDBC implementation.
* <p>
* This overloaded variant always attempts to hydrate and return the result as {@code Record}s.
*
* @param fql
* FQL query statement.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list.
* If this value is non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this
* value is non-positive, an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
*
* @return A list of results, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> List<T> list(String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation)
throws PersistenceException
{
return list(null, fql, values, maxResults, startOffset, relation, true);
}
/**
* Execute an FQL query and return the results as a list. If the query returns multiple results
* per row, each element in the list will be an array of {@code Object}s.
* <p>
* No locking is attempted.
* <p>
* <strong>Note:</strong><br>
* This method should be used only for queries that are expected to return a relatively small
* result set, since the entire result set is stored in the returned list. Additionally, some
* JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
* set in memory, effectively doubling the amount of memory required. To avoid this, consider
* using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
* Using that method with a
* {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
* value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
* results at a time. Consult your JDBC driver documentation and/or source code to determine
* whether this is an issue for your JDBC implementation.
*
* @param <T>
* Type of object being listed.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be processed. This method makes no assumptions as to the
* types of object(s) returned.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is
* non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive,
* an offset of 0 is used by default.
* @param hydrateRecords
* If {@code true} the result is interpreted as legacy records, and they will be hydrated.
* Otherwise, the returned {@code List} contains an array of Java objects for each row.
*
* @return A list of results, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> List<T> list(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
boolean hydrateRecords)
throws PersistenceException
{
return list(entities, fql, values, maxResults, startOffset, null, hydrateRecords);
}
/**
* Execute an FQL query and return the results as a list. If the query returns multiple results
* per row, each element in the list will be an array of {@code Object}s.
* <p>
* No locking is attempted.
* <p>
* <strong>Note:</strong><br>
* This method should be used only for queries that are expected to return a relatively small
* result set, since the entire result set is stored in the returned list. Additionally, some
* JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
* set in memory, effectively doubling the amount of memory required. To avoid this, consider
* using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
* Using that method with a
* {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
* value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
* results at a time. Consult your JDBC driver documentation and/or source code to determine
* whether this is an issue for your JDBC implementation.
*
* @param <T>
* Type of object being listed.
*
* @param entities
* Names of the DMO entities associated with this query statement (Unused parameter).
* @param fql
* FQL query statement to be processed. This method makes no assumptions as to the
* types of object(s) returned.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param maxResults
* The maximum number of elements to be returned in the list. If this value is
* non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive,
* an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param hydrateRecords
* If {@code true} the result is interpreted as legacy records, and they will be hydrated.
* Otherwise, the returned {@code List} contains an array of Java objects for each row.
*
* @return A list of results, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error executing the query.
*/
public <T> List<T> list(Class<? extends Record>[] entities,
String fql,
Object[] values,
int maxResults,
int startOffset,
DataRelation relation,
boolean hydrateRecords)
throws PersistenceException
{
Context local = getContext(entities);
List<T> results = null;
try
{
Query query = getQuery(local, fql, maxResults, startOffset, relation, values);
results = query.list(local.getSession(), hydrateRecords);
}
catch (UdfException exc)
{
if (!local.isUnsafe())
{
cleanup();
ErrorEntry ee = exc.getErrorEntry();
ErrorManager.recordOrShowError(
new int[] {ee.num},
new String[] {ee.text},
true,
ee.prefix,
false,
false,
false,
true);
handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
}
else
{
throw exc;
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "error executing query [" + fql + "]");
}
return (results == null || results.isEmpty() ? null : results);
}
/**
* Execute an FQL query and load a single record from the session-level cache or, optionally,
* refresh it from the database. The record will be locked with the specified lock type upon
* return from this method. Note that the lock refers to a runtime-level lock, not a
* database-level lock. The record is <i>not</i> automatically loaded into the record buffer
* provided. The <code>buffer</code> argument is used only to extract information needed
* to perform the query.
*
* @param buffer
* Record buffer which holds information about the DMO class type
* and name of the table being queried.
* @param fql
* FQL query statement. This may be either an ID-only projection query or a full query
* (see above).
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param lockType
* Type of lock the lock manager should register on the resulting
* record for the current user context.
* @param timeout
* Number of milliseconds to wait to acquire the requested lock type.
* Throws {@link LockTimeoutException} if this time elapses before the
* lock is acquired. Specify 0 to wait indefinitely for the lock. Ignored
* if {@code lockType} is {@code NONE}.
* @param unique
* {@code true} if no more than one result is expected from this query;
* {@code false} if the query could result in more than one record.
* @param updateLock
* {@code true} to update the current lock state;
* {@code false} to leave the current lock state unchanged.
* @param forceRefresh
* If {@code true} forces the record to be refreshed from database. Otherwise
* it will be refreshed only if the lockType has been upgraded from NONE.
* @param findByKey
* If {@code true}, this overrides the current {@code rereadnolock} setting;
* if {@code false}, honor {@code rereadnolock}.
*
* @return A single DMO instance representing the query result, or {@code null} if no result
* was found.
*
* @throws LockTimeoutException
* if a record was found, but its lock could not be acquired within the positive
* {@code timeout} period specified.
* @throws LockUnavailableException
* if a record was found and a no-wait lock type was specified, but the lock could
* not be acquired immediately.
* @throws PersistenceException
* if there was an error executing the query or if a unique query
* (i.e., {@code unique == true}) returned multiple results,
*/
public Record load(RecordBuffer buffer,
String fql,
Object[] values,
LockType lockType,
long timeout,
boolean unique,
boolean updateLock,
boolean forceRefresh,
boolean findByKey)
throws PersistenceException
{
boolean trace = LOG.isLoggable(Level.FINEST);
boolean warn = (trace || LOG.isLoggable(Level.WARNING));
boolean sharedDb = !buffer.getDmoInfo().multiTenant;
Context local = getContext(sharedDb);
boolean inlineLimit = (values == null || values.length == 0);
Long id = null;
Record initialResult = null;
Record result = null;
String table = buffer.getTable();
LockType oldLock = null;
RecordLockContext lockContext = local.getRecordLockContext();
Session session = local.getSession();
if (temporary)
{
updateLock = false;
}
try
{
// The inlineLimit flag is a crude test to detect a "simple" query:
// no substitution parameters. We want to avoid inlining the limit
// on more complex statements, as the intended performance benefit
// may backfire in these cases.
dialect.inlineLimit(inlineLimit);
// We have to give queries which are supposed to return a unique
// result enough rope to hang themselves, so set max results to 2
// for these. All other queries are limited to 1 result.
String entity = buffer.getEntityName();
boolean readOnly = !temporary && DmoMetadataManager.isReadOnly(entity);
Query query = getQuery(local, fql, unique ? 2 : 1, 0, null, values);
// Reset ambiguous flag in buffer.
buffer.setAmbiguous(false);
// Find the record.
Object o = query.uniqueResult(session);
// Reset limit inlining if necessary.
if (inlineLimit)
{
dialect.inlineLimit(false);
inlineLimit = false;
}
if (o == null)
{
// No hit.
return null;
}
if (o instanceof Record)
{
// This is the fast path where we assume we retrieved the DMO
// directly instead of the ID because we don't have to lock
// (we might need to unlock, however).
// This is the case for temp-tables and no-lock queries.
initialResult = result = (Record) o;
// Lock record if required, remembering old lock type in case we
// need to roll back on error.
if (updateLock)
{
id = result.primaryKey();
RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
oldLock = queryLock(ident, sharedDb);
if (oldLock.compareTo(lockType) != 0)
{
lockContext.lock(ident, lockType, timeout, null);
// ensure that if we have acquired a new lock, we have the latest version of
// the record from the database
if (!readOnly && !forceRefresh && oldLock == LockType.NONE)
{
// LOG.info("LOAD refresh (lock acquisition 1) [" + fql + "]");
forceRefresh = true;
}
}
}
// If we found the record with a non-primary-key query and it already exists in any
// buffer, it is possible it came from the session cache and needs to be refreshed
if (!forceRefresh &&
!temporary &&
!readOnly &&
oldLock == LockType.NONE &&
lockType == LockType.NONE &&
!findByKey &&
local.isRereadNoLock())
{
// if any other buffer references this DMO already, it means we got it from
// the session cache and it might be stale, so force a refresh
// TODO: goal is to make refresh unnecessary by knowing whether record came from
// session cache or database
/*
int useCount = local.bufferManager.getDMOUseCount(result);
if (useCount > 0)
{
// LOG.info("LOAD refresh (-rereadnolock 1) [" + fql + "]");
forceRefresh = true;
}
*/
}
}
else
{
// If we got here, we're dealing with a primary key query.
id = (Long) o;
RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
// lock record if required, remembering old lock type in case we need to roll back on error
if (updateLock)
{
oldLock = queryLock(ident, sharedDb);
lockContext.lock(ident, lockType, timeout, null);
}
else
{
lock(lockType, ident, false, sharedDb);
}
// We use get() instead of load() here because we don't want to return a proxy and
// have to take another hit retrieving the data almost immediately after this method
// returns, which is the common case for dynamic record retrieval.
Class<? extends Record> dmoClass = buffer.getDMOImplementationClass();
initialResult = result = session.get(dmoClass, id, null);
if (result == null)
{
return null;
}
// Ensure that if we are acquiring a lock, we have the latest version of the record
// from the database. The Hibernate API advertises that Session.get() with a LockMode
// of READ should bypass both levels of cache, but debugging indicates that the
// record is always taken from the session cache if present.
// So, we brute force a refresh instead.
if (!forceRefresh && updateLock && oldLock == LockType.NONE && !readOnly)
{
if (lockType != LockType.NONE)
{
// LOG.info("LOAD refresh (lock acquisition 2) [" + fql + "]");
// forced refresh if lock is upgraded
forceRefresh = true;
}
// possibly force refresh if rereadnolock is enabled, and query is not by
// recid/rowid
// TODO: goal is to make refresh unnecessary
/*
else if (lockType == LockType.NONE && !findByKey && local.isRereadNoLock())
{
// if any other buffer references this DMO already, it means we got it from
// the session cache and it might be stale (since we have no lock on it), so
// force a refresh
int useCount = local.bufferManager.getDMOUseCount(result);
if (useCount > 0)
{
// LOG.info("LOAD refresh (-rereadnolock 2) [" + fql + "]");
forceRefresh = true;
}
}
*/
}
}
// The Hibernate API advertises that Session.get() with a LockMode of READ should
// bypass both levels of cache, but debugging indicates that the record is always
// taken from the session cache if present. So, we brute force a refresh instead.
// TODO: goal is to make refresh unnecessary
/*
if (!readOnly && forceRefresh)
{
Session session = local.getSession();
session.refresh(result);
}
*/
}
catch (UniqueResultException exc)
{
// This is recoverable, no need to close session.
uniqueResultViolation(buffer, exc);
}
catch(UdfException exc)
{
result = null;
ErrorEntry ee = exc.getErrorEntry();
ErrorManager.recordOrShowError(
new int[] {ee.num},
new String[] {ee.text},
true,
ee.prefix,
false,
false,
false,
true);
handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
}
catch (MissingRecordException | LockUnavailableException exc)
{
// the initial result we found must be evicted from the session cache
if (initialResult != null) // NOTE: condition is always false
{
if (session != null)
{
session.evict(initialResult);
}
}
// Catching this exception indicates we were holding a stale copy of
// a record that was deleted in the meantime. This can happen in
// bulk delete cases, or when a preselect query is holding an old
// reference. This is not a fatal error, it just means that the
// refresh failed and the result is null.
result = null;
throw exc;
}
catch (PersistenceException exc)
{
if (!local.isUnsafe())
{
DBUtils.handleException(local.tenant.database, exc);
// if exc was caused by ConditionException and
// ErrorManager is in CAN-FIND mode, let the session alive
Throwable cause = exc.getCause();
if (cause != null && !(cause instanceof ConditionException))
{
cause = cause.getCause();
}
if (cause == null && !buffer.txHelper.errHlp.isInCanFindBracket())
{
local.closeSession(true);
}
String msg = message("error loading record");
if (warn)
{
LOG.log(Level.WARNING, msg + System.lineSeparator() + fql);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Details:", exc);
}
}
handleException(msg, exc);
}
else
{
throw exc;
}
}
finally
{
// Reset limit inlining if (still) necessary.
if (inlineLimit)
{
dialect.inlineLimit(false);
}
if (updateLock &&
id != null &&
result == null &&
oldLock != null &&
oldLock.compareTo(lockType) != 0)
{
// Reset lock to its previous state. This is required if another
// thread deleted the record after we got its ID but before we
// retrieved its data.
try
{
RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
if (oldLock == LockType.NONE)
{
lockContext.forceRelease(ident, null);
}
else
{
lockContext.lock(ident, oldLock.toNoWaitVariant());
}
}
catch (LockUnavailableException exc)
{
// OK to ignore.
}
}
}
return result;
}
/**
* Load a single record from the session-level cache or, optionally, refresh it from the
* database. The record will be locked with the specified lock type upon return from this
* method. Note that the lock refers to a runtime-level lock, not a database-level lock.
*
* @param implClass
* The DMO implementation class of record being read.
* @param id
* Primary key of the record to be loaded into the buffer. May not be {@code null}.
* @param lockType
* Type of lock the lock manager should register on the resulting record for the
* current user context.
* @param timeout
* Number of milliseconds to wait to acquire the requested lock type. Throws
* {@link LockTimeoutException} if this time elapses before the lock is acquired.
* Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
* {@code NONE}.
* @param updateLock
* {@code true} to update the current lock state;
* {@code false} to leave the current lock state unchanged.
*
* @return A single DMO instance loaded from the database, or {@code null} if no result was
* found.
*
* @throws LockTimeoutException
* if a record was found, but its lock could not be acquired within the positive
* {@code timeout} period specified.
* @throws LockUnavailableException
* if a record was found and a no-wait lock type was specified, but the lock could
* not be acquired immediately.
* @throws PersistenceException
* if there was an error loading the DMO from the ORM session and/or database.
*/
public Record load(Class<? extends Record> implClass,
Long id,
LockType lockType,
long timeout,
boolean updateLock)
throws PersistenceException,
LockTimeoutException
{
return load(implClass, id, lockType, timeout, updateLock, null);
}
/**
* Load a single record from the session-level cache or, optionally, refresh it from the
* database. The record will be locked with the specified lock type upon return from this
* method. Note that the lock refers to a runtime-level lock, not a database-level lock.
*
* @param implClass
* The DMO implementation class of record being read.
* @param id
* Primary key of the record to be loaded into the buffer. May not be {@code null}.
* @param lockType
* Type of lock the lock manager should register on the resulting record for the
* current user context.
* @param timeout
* Number of milliseconds to wait to acquire the requested lock type. Throws
* {@link LockTimeoutException} if this time elapses before the lock is acquired.
* Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
* {@code NONE}.
* @param updateLock
* {@code true} to update the current lock state;
* {@code false} to leave the current lock state unchanged.
* @param partialFields
* For an incomplete record, read only these fields.
*
* @return A single DMO instance loaded from the database, or {@code null} if no result was
* found.
*
* @throws LockTimeoutException
* if a record was found, but its lock could not be acquired within the positive
* {@code timeout} period specified.
* @throws LockUnavailableException
* if a record was found and a no-wait lock type was specified, but the lock could
* not be acquired immediately.
* @throws PersistenceException
* if there was an error loading the DMO from the ORM session and/or database.
*/
public Record load(Class<? extends Record> implClass,
Long id,
LockType lockType,
long timeout,
boolean updateLock,
BitSet partialFields)
throws PersistenceException,
LockTimeoutException
{
Record result = null;
LockType oldLock = LockType.NONE;
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(implClass);
Context local = getContext(!dmoInfo.multiTenant);
String table = dmoInfo.getSqlTableName();
RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
RecordLockContext lockContext = local.getRecordLockContext();
boolean resetLock = false;
boolean vstTable = false;
if (temporary)
{
updateLock = false;
}
if (local.tenant.database.isMeta())
{
// TODO: use a more efficient way to detect VST non-locking tables
boolean isTrans = table.endsWith("_trans");
boolean isLock = table.endsWith("_lock");
// TODO: add all VST non-locking tables
if (isTrans || isLock)
{
vstTable = true;
updateLock = false;
}
}
try
{
// lock record if required, remembering old lock type in case we need to roll back on error
if (updateLock)
{
oldLock = queryLock(ident, !dmoInfo.multiTenant);
lockContext.lock(ident, lockType, timeout, null);
}
else if (!vstTable)
{
lock(lockType, ident, false, !dmoInfo.multiTenant);
}
Session session = local.getSession();
result = session.get(implClass, id, partialFields);
if (result == null)
{
resetLock = true;
return null;
}
}
finally
{
if (resetLock && updateLock && oldLock.compareTo(lockType) != 0)
{
// reset lock to its previous state in case another thread deleted the record after
// we got its ID but before we retrieved its data
try
{
if (oldLock == LockType.NONE)
{
lockContext.forceRelease(ident, null);
}
else
{
lockContext.lock(ident, oldLock.toNoWaitVariant());
}
}
catch (LockUnavailableException luExc)
{
// OK to ignore.
}
}
}
return result;
}
/**
* Load a single record from the database or from the session-level cache.
* No locking is attempted, the assumption being that any required lock
* previously was acquired.
*
* @param ident
* Record identifier of the record to be loaded. Consists of the
* primary key and the fully qualified DMO entity name.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @return A single DMO instance loaded from the database, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error creating an ORM session or getting the requested record.
*/
@Override
public Record quickLoad(RecordIdentifier<String> ident, boolean sharedDb)
throws PersistenceException
{
return quickLoad(ident, false, sharedDb);
}
/**
* Load a single record from the database or from the session-level cache. No locking is attempted, the
* assumption being that any required lock previously was acquired.
*
* @param ident
* Record identifier of the record to be loaded. Consists of the primary key and the fully
* qualified DMO entity name.
* @param refresh
* If {@code true} then re-read the state of the record from the underlying database. This is
* useful when record has been changed bypassing the FWD ORM.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @return A single DMO instance loaded from the database, or {@code null} if no result was found.
*
* @throws PersistenceException
* if there was an error creating an ORM session or getting the requested record.
*/
@Override
public Record quickLoad(RecordIdentifier<String> ident, boolean refresh, boolean sharedDb)
throws PersistenceException
{
Context local = getContext(sharedDb);
String entity = ident.getTable();
Long id = ident.getRecordID();
Session session = local.getSession();
Record result = null;
boolean readOnly = !temporary && DmoMetadataManager.isReadOnly(entity);
if (readOnly)
{
refresh = false;
}
try
{
result = session.get(entity, id);
if (refresh && result != null)
{
// LOG.info("QUICKLOAD refresh [" + ident + "]");
session.refresh(result);
}
}
catch (PersistenceException exc)
{
String msg = message("error executing quick load [" + entity + ":" + id + "]");
handleException(local, exc, msg);
}
return result;
}
/**
* Execute an arbitrary SQL statement which does not produce a result set.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #executeSQL(String, boolean)} instead.
*
* @param sql
* SQL statement text.
*
* @return The number of entities updated or deleted, or {@code -1} if the statement
* did not complete normally.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
public int executeSQL(String sql)
throws PersistenceException
{
return executeSQL(sql, Persistence.PRIVATE_CTX, null, false);
}
/**
* Execute an arbitrary SQL statement which does not produce a result set.
* <br><b>Note:</b>
* Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
* this statement. This cannot be done here because the method is not aware of the buffer list and if
* multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
* of then.
*
* @param sql
* SQL statement text.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @return The number of entities updated or deleted, or {@code -1} if the statement
* did not complete normally.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public int executeSQL(String sql, boolean sharedDb)
throws PersistenceException
{
return executeSQL(sql, sharedDb, null, false);
}
/**
* Execute an arbitrary SQL statement which does not produce a result set.
* <br><b>Note:</b>
* Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
* this statement. This cannot be done here because the method is not aware of the buffer list and if
* multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
* of them.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #rollback(boolean)} instead.
*
* @param sql
* SQL statement text.
* @param args
* The statement arguments.
*
* @return The number of entities updated or deleted, or {@code -1} if the statement
* did not complete normally.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
public int executeSQL(String sql, Object[] args)
throws PersistenceException
{
return executeSQL(sql, SHARED_CTX, args, false);
}
/**
* Execute an arbitrary SQL statement which does not produce a result set.
* <br><b>Note:</b>
* Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
* this statement. This cannot be done here because the method is not aware of the buffer list and if
* multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
* of them.
*
* @param sql
* SQL statement text.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param args
* The statement arguments.
*
* @return The number of entities updated or deleted, or {@code -1} if the statement
* did not complete normally.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public int executeSQL(String sql, boolean sharedDb, Object[] args)
throws PersistenceException
{
return executeSQL(sql, sharedDb, args, false);
}
/**
* Execute an arbitrary SQL query.
* <p>
* It is the responsibility of the calling code to close the returned result set. In addition, if this
* method is called outside a database transaction, the caller should optionally invoke
* {@link Persistence#beginTransaction(boolean)} before calling this method and
* {@link Persistence#commit(boolean)} or {@link Persistence#rollback(boolean)} after fully processing the
* result set returned by this method. This will ensure a transaction is opened and properly closed if
* necessary.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #executeSQLQuery(String, boolean, Object[])} instead.
*
* @param sql
* SQL query text.
* @param args
* The query parameters.
*
* @return Result set produced by query.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public <T> ScrollableResults<T> executeSQLQuery(String sql, Object[] args)
throws PersistenceException
{
return executeSQLQuery(sql, PRIVATE_CTX, args);
}
/**
* Execute an arbitrary SQL query.
* <p>
* It is the responsibility of the calling code to close the returned result set. In addition, if this
* method is called outside a database transaction, the caller should optionally invoke
* {@link Persistence#beginTransaction(boolean)} before calling this method and
* {@link Persistence#commit(boolean)} or {@link Persistence#rollback(boolean)} after fully processing the
* result set returned by this method. This will ensure a transaction is opened and properly closed if
* necessary.
*
* @param sql
* SQL query text.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param args
* The query parameters.
*
* @return Result set produced by query.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public <T> ScrollableResults<T> executeSQLQuery(String sql, boolean sharedDb, Object[] args)
throws PersistenceException
{
long start = 0L;
boolean trace = LOG.isLoggable(Level.FINEST);
if (trace)
{
start = System.currentTimeMillis();
}
Context local = getContext(sharedDb);
Session session = local.getSession();
ScrollableResults<T> results = null;
try
{
SQLQuery query = Session.createSQLQuery(sql);
if (args != null)
{
for (int i = 0; i < args.length; i++)
{
query.setParameter(i, args[i]);
}
}
results = query.scroll(session, null);
}
catch (PersistenceException exc)
{
handleException(local, exc, "Error executing SQL statement: " + sql);
}
finally
{
if (trace)
{
long elapsed = System.currentTimeMillis() - start;
LOG.log(Level.FINEST, message("TIMING:SQLQ [" + elapsed + "] " + sql));
}
}
return results;
}
/**
* Convenience method to retrieve a single result from a SQL statement. This is intended to
* be used for SQL statements that return only one result. Only the first result is checked;
* if there is more than one in the result set, all but the first are ignored.
* <p>
* The result set and statement are closed immediately; no additional cleanup is required by the caller.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #getSingleSQLResult(String, boolean, Object[])} instead.
*
* @param sql
* SQL statement to execute.
* @param args
* Query substitution parameters, if any. If present, these should match the number
* of placeholders in the SQL statement. If not present, this argument may either be
* {@code null} or an empty array.
*
* @return The first (and presumably only) result returned by the SQL statement.
*
* @throws PersistenceException
* if any error occurs, either during the statement's execution, or when pushing or
* popping an implicit transaction.
*/
@Override
public <T> T getSingleSQLResult(String sql, Object[] args)
throws PersistenceException
{
return getSingleSQLResult(sql, PRIVATE_CTX, args);
}
/**
* Convenience method to retrieve a single result from a SQL statement. This is intended to
* be used for SQL statements that return only one result. Only the first result is checked;
* if there is more than one in the result set, all but the first are ignored.
* <p>
* The result set and statement are closed immediately; no additional cleanup is required by the caller.
*
* @param sql
* SQL statement to execute.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param args
* Query substitution parameters, if any. If present, these should match the number
* of placeholders in the SQL statement. If not present, this argument may either be
* {@code null} or an empty array.
*
* @return The first (and presumably only) result returned by the SQL statement.
*
* @throws PersistenceException
* if any error occurs, either during the statement's execution, or when pushing or
* popping an implicit transaction.
*/
@Override
public <T> T getSingleSQLResult(String sql, boolean sharedDb, Object[] args)
throws PersistenceException
{
Context local = getContext(sharedDb);
// if the context has an active session, use it; otherwise, create a short-lived session
Session session = local.getSessionNoCreate();
boolean shortLived = session == null;
boolean commit;
if (shortLived)
{
session = new Session(local.tenant.database, null, null, local.getTenantName());
commit = session.beginTransaction();
}
else
{
commit = local.beginTransaction(null);
}
try
{
try (PreparedStatement stmt = session.getConnection().prepareStatement(sql))
{
if (args != null)
{
int len = args.length;
for (int i = 0; i < len; i++)
{
stmt.setObject(i + 1, args[i]);
}
}
QueryStatement f = PreparedStatement::executeQuery;
ResultSet rs = SQLExecutor.getInstance().execute(local.tenant.database, sql, f, stmt);
return rs.next() ? (T) rs.getObject(1) : null;
}
catch (SQLException e)
{
throw new PersistenceException("Failed to execute [" + sql + "]", e);
}
}
catch (PersistenceException exc)
{
if (!local.isUnsafe())
{
commit = false;
DBUtils.handleException(local.tenant.database, exc);
if (shortLived)
{
session.rollback();
}
else
{
local.closeSession(true);
}
String msg = "Error executing SQL statement: " + sql;
handleException(msg, exc);
return null;
}
else
{
throw exc;
}
}
finally
{
if (shortLived)
{
if (commit)
{
session.commit();
}
try
{
session.close();
}
catch (PersistenceException exc)
{
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, "Error closing session", exc);
}
}
}
else if (commit)
{
local.commit();
}
}
}
/**
* Execute an arbitrary list of SQL statements in batch.
*
* @param sql
* List of SQL statements to execute in batch.
* @param noFlush
* If {@code true}, flushing is bypassed.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @throws PersistenceException
* if any error occurs executing the statements.
*/
public void executeSQLBatch(List<String> sql, boolean noFlush, boolean sharedDb)
throws PersistenceException
{
executeSQLBatch(getContext(sharedDb), sql, noFlush);
}
/**
* Attempt to obtain the specified lock type on a particular record. If the lock cannot be
* obtained immediately, the current thread will block until the lock becomes available and
* it is obtained, unless the requested lock type is a "no-wait" lock.
* <p>
* In the no-wait case, an exception is raised indicating that the lock is not available.
* <p>
* The current thread continues normally once the lock is obtained.
* <p>
* A lock currently held can be released by specifying {@code LockType.NONE} for the
* {@code lockType} parameter.
* <p>
* The normal return of this method indicates that the lock request (or lack thereof)
* completed successfully, and that the current context now holds a lock of the requested
* type.
* <p>
* NOTE: legacy persistence framework code should access this method only via a {@link
* RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
* {@code update == false}). This ensures that record locks shared across record buffers
* within a client context are managed properly. Non-legacy code may access this API
* directly.
*
* @param lockType
* Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
* lock, and to continue with no lock.
* @param ident
* ID which uniquely identifies the record being locked.
* @param update
* {@code true} if the lock state for the current record should be updated;
* else {@code false}.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @throws LockUnavailableException
* if a "no-wait" lock cannot be acquired immediately.
*
* @see #getRecordLockContext
*/
public void lock(LockType lockType, RecordIdentifier<String> ident, boolean update, boolean sharedDb)
throws LockUnavailableException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
{
tenant.lockManager.get().lock(lockType, ident, update);
}
}
/**
* Attempt to obtain the specified lock type on a particular record. If the lock cannot be
* obtained immediately, the current thread will block until the lock becomes available and
* it is obtained, unless the requested lock type is a "no-wait" lock.
* <p>
* In the no-wait case, an exception is raised indicating that the lock is not available.
* <p>
* The current thread continues normally once the lock is obtained.
* <p>
* A lock currently held can be released by specifying {@code LockType.NONE} for the
* {@code lockType} parameter.
* <p>
* The normal return of this method indicates that the lock request (or lack thereof)
* completed successfully, and that the current context now holds a lock of the requested
* type.
* <p>
* NOTE: legacy persistence framework code should access this method only via a {@link
* RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
* {@code update == false}). This ensures that record locks shared across record buffers
* within a client context are managed properly. Non-legacy code may access this API
* directly.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #lock(LockType, RecordIdentifier, boolean, long, boolean)} instead.
*
* @param lockType
* Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
* lock, and to continue with no lock.
* @param ident
* ID which uniquely identifies the record being locked.
* @param update
* {@code true} if the lock state for the current record should be updated; else {@code false}.
*
* @throws LockUnavailableException
* if a "no-wait" lock cannot be acquired immediately.
*
* @see #getRecordLockContext
*/
public void lock(LockType lockType, RecordIdentifier<String> ident, boolean update)
throws LockUnavailableException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(PRIVATE_CTX).tenant;
if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
{
tenant.lockManager.get().lock(lockType, ident, update);
}
}
/**
* Attempt to obtain the specified lock type on a particular record. If the lock cannot be
* obtained immediately, the current thread will block until the lock becomes available and
* it is obtained, unless the requested lock type is a "no-wait" lock, or a positive timeout
* value is specified and that time elapses before the lock is acquired.
* <p>
* In the no-wait case, an exception is raised indicating that the lock is not available.
* In the timeout case, an exception is raised indicating that the timeout period elapsed.
* <p>
* The current thread continues normally once the lock is obtained.
* <p>
* A lock currently held can be released by specifying {@code LockType.NONE} for the
* {@code lockType} parameter.
* <p>
* The normal return of this method indicates that the lock request (or lack thereof)
* completed successfully, and that the current context now holds a lock of the requested
* type.
* <p>
* NOTE: legacy persistence framework code should access this method only via a {@link
* RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
* {@code update == false}). This ensures that record locks shared across record buffers
* within a client context are managed properly. Non-legacy code may access this API
* directly.
*
* @param lockType
* Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
* lock, and to continue with no lock.
* @param ident
* ID which uniquely identifies the record being locked.
* @param update
* {@code true} if the lock state for the current record should be updated; else {@code false}.
* @param timeout
* Number of milliseconds to wait to acquire the requested lock type.
* Throws {@link LockTimeoutException} if this time elapses before the
* lock is acquired. Specify 0 to wait indefinitely for the lock. Ignored
* if {@code lockType} is {@code NONE}.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @throws LockTimeoutException
* if a lock cannot be acquired within the positive {@code timeout} period specified.
* @throws LockUnavailableException
* if a "no-wait" lock cannot be acquired immediately.
*
* @see #getRecordLockContext
*/
public void lock(LockType lockType,
RecordIdentifier<String> ident,
boolean update,
long timeout,
boolean sharedDb)
throws LockUnavailableException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
{
tenant.lockManager.get().lock(lockType, ident, update, timeout);
}
}
/**
* Attempt to obtain the specified lock type on a table. If the lock cannot be obtained
* immediately, the current thread will block until the lock becomes available and is
* obtained, unless the requested lock type is a "no-wait" lock. In the latter case,
* an exception is raised, indicating that the lock is not available. The current thread
* continues once the lock is obtained.
* <p>
* A lock currently held can be released by specifying
* <code>LockType.NONE</code> for the <code>lockType</code> parameter.
* <p>
* Method returns previous lock type (so that calling code knows what to set it back to after
* bulk delete).
* The normal return of this method indicates that the lock request (or lack thereof)
* completed successfully, and that the current context now holds a lock of the requested
* type.
*
* @param table
* Table name.
* @param lockType
* Type of lock to be obtained. <code>LockType.NONE</code> is used to release an
* existing lock, and to continue with no lock.
* @param update
* {@code true} if the lock state for the current table should be updated; else {@code false}.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @throws LockUnavailableException
* if a "no-wait" lock cannot be acquired immediately.
*
* @return previous lock type.
*/
public LockType lockTable(String table, LockType lockType, boolean update, boolean sharedDb)
throws LockUnavailableException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
{
return tenant.lockManager.get().lockTable(lockType, table, update);
}
return LockType.NONE;
}
/**
* Query the lock type currently held by the current context for the
* specified database record.
* <p>
* This method will always return the <b>non-</b><code>NO_WAIT</code>
* variants of a lock type. That is, even if the actual lock type is
* <code>LockType.EXCLUSIVE_NO_WAIT</code> or
* <code>LockType.SHARE_NO_WAIT</code>, the simpler types of
* <code>LockType.EXCLUSIVE</code> and <code>LockType.SHARE</code>,
* respectively, are returned. The lack of a lock returns
* <code>LockType.NONE</code> rather than <code>null</code>.
*
* @param ident
* ID which uniquely identifies the record being queried.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @return The lock type currently held by the current context, never {@code null}.
*/
public LockType queryLock(RecordIdentifier<String> ident, boolean sharedDb)
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
if (tenant.lockManager.wasSet() && tenant.lockManager.get() == null)
{
return LockType.NONE;
}
return tenant.lockManager.get().queryLock(ident);
}
/**
* Obtain the next available primary key ID on a per-table basis.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #nextPrimaryKey(String, boolean)} instead.
*
* @param table
* Table for which next primary key will be returned.
*
* @return The next available ID for the given table.
*
* @throws PersistenceException
* if there is an error determining the next primary key ID.
* @throws IllegalStateException
* if identity manager was not initialized.
*/
public Long nextPrimaryKey(String table)
throws PersistenceException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(PRIVATE_CTX).tenant;
if (tenant.identityManager.wasSet() && tenant.identityManager.get() != null)
{
return tenant.identityManager.get().nextPrimaryKey(table);
}
throw new IllegalStateException("identity manager should be initialized");
}
/**
* Obtain the next available primary key ID on a per-table basis.
*
* @param table
* Table for which next primary key will be returned.
* @param shared
* Only used if a regular tenant is active.<br>
* If {@code true} the lock manager for default (shared/common) database is returned.
* Otherwise, the one of the tenant set on current context is returned.
*
* @return The next available ID for the given table.
*
* @throws PersistenceException
* if there is an error determining the next primary key ID.
* @throws IllegalStateException
* if identity manager was not initialized.
*/
public Long nextPrimaryKey(String table, boolean shared)
throws PersistenceException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(shared).tenant;
if (tenant.identityManager.wasSet() && tenant.identityManager.get() != null)
{
return tenant.identityManager.get().nextPrimaryKey(table);
}
throw new IllegalStateException("identity manager should be initialized");
}
/**
* Updates the existing record in the backing database session. It is assumed this method will be invoked only
* within a database transaction.
*
* @param dmo
* DMO to be persisted.
*
* @throws PersistenceException
* if a problem occurs saving the object in the current session.
*/
public void save(Record dmo)
throws PersistenceException
{
save(dmo, dmo.primaryKey(), true);
}
/**
* Persist the specified object to the backing database session, using the given ID. It is assumed
* this method will be invoked only within a database transaction.
*
* @param dmo
* DMO to be persisted.
* @param id
* Primary key to associate with the persisted DMO.
*
* @throws PersistenceException
* if a problem occurs saving the object in the current session.
*/
public void save(Record dmo, Long id)
throws PersistenceException
{
save(dmo, id, true);
}
/**
* Persist the specified object to the backing database session, using the given ID. It is assumed
* this method will be invoked only within a database transaction.
*
* @param dmo
* DMO to be persisted.
* @param id
* Primary key to associate with the persisted DMO.
* @param updateDmoState
* {@code true} to update the record state of the DMO. Should be {@code false} for non-legacy
* use cases).
*
* @throws PersistenceException
* if a problem occurs saving the object in the current session.
*/
public void save(Record dmo, Long id, boolean updateDmoState)
throws PersistenceException
{
Long dmoKey = dmo.primaryKey();
if (dmoKey != null && dmoKey < 0)
{
// this should not happen: we have already tested this in RecordBuffer in the event of
// a field change. Since the template record cannot be altered, it should also not need
// to be flushed/saved to persistence.
final String msg = "Cannot update a template record without a schema lock";
ErrorManager.recordOrThrowError(12376, msg, false, false);
// print the error as SEVERE
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
"Template records MUST not be flushed/saved to database!",
new PersistenceException("Saving template records to database is forbidden"));
}
return;
}
Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
boolean inTx = local.beginTransaction(null);
try
{
if (dmoKey == null)
{
dmo.primaryKey(id);
}
local.getSession().save(dmo, updateDmoState);
if (inTx)
{
local.commit();
}
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, message("Persisted DMO (" + dmo + ") with ID " + id));
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "error persisting object");
}
}
/**
* Merges the state of the given object with the persistent object with the same identifier and
* returns the merged object.
*
* @param dmo
* DMO to merge.
*
* @return Merged DMO.
*
* @throws PersistenceException
* in error
*
* @deprecated
*/
@Deprecated
public Record merge(Record dmo)
throws PersistenceException
{
Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
Record merged = null;
try
{
merged = local.getSession().merge(dmo);
// Hibernate's Session.merge() saves the record if not done already.
// The caller must mark the dmo as persisted.
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, message("Merged DMO (" + dmo + ")"));
}
}
catch (PersistenceException exc)
{
DBUtils.handleException(local.tenant.database, exc);
local.closeSession(true);
handleException("error merging object", exc);
}
return merged;
}
/**
* Get the ORM session associated with the current context, creating one if needed.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #getSession(boolean)} instead.
*
* @return ORM session for the current context.
*/
public Session getSession()
{
try
{
return getContext(PRIVATE_CTX).getSession();
}
catch (PersistenceException e)
{
throw new IllegalStateException("Cannot get database session");
}
}
/**
* Get the ORM session associated with the current context, creating one if needed.
*
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @return ORM session for the current context.
*/
public Session getSession(boolean sharedDb)
{
try
{
return getContext(sharedDb).getSession();
}
catch (PersistenceException e)
{
throw new IllegalStateException("Cannot get database session");
}
}
/**
* Get the context for the current user session.
* <p>
* NOTE: this method is package private and should remain so. It is meant to allow optimized
* calls to certain frequently used methods of this class, in order to avoid the overhead of
* an unnecessary context local lookup.
*
* @param sharedDb
* In case of tenant databases, if this is {@code true}, the common (default/shared) context is
* returned. Otherwise, the private context of the current tenant is returned. If there is no
* active tenant, the context of the default database is returned.
*
* @return User context.
*/
public Context getContext(boolean sharedDb)
{
Context[] ctxs = contexts.get();
// even if the private context is requested, the shared instance is returned if private was not created
return ctxs[sharedDb || ctxs[PRIVATE_IDX] == null ? SHARED_IDX : PRIVATE_IDX];
}
/**
* Clean up the current contexts.
*/
public void cleanup()
{
Context[] ctxt = contexts.get(false);
if (ctxt != null)
{
ctxt[SHARED_IDX].cleanup();
if (ctxt[PRIVATE_IDX] != null)
{
ctxt[PRIVATE_IDX].cleanup();
}
}
}
/**
* Delete the persisted object from the specified database.
*
* @param dmo
* Persistent entity to be deleted.
*
* @throws PersistenceException
* if a problem occurs deleting the object.
*/
public void delete(Record dmo)
throws PersistenceException
{
Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
try
{
Session session = local.getSession();
session.delete(dmo);
}
catch (PersistenceException exc)
{
handleException(local, exc, "error deleting persistent object");
}
}
/**
* Perform a bulk delete or update of all objects which meet the specified
* criteria. The criteria must indicate a single DMO type only.
* <p>
* This action must take place within a database transaction, so this
* method attempts to begin one and commit it, if one is not already
* active.
* <p>
* This method should only be used for temp-tables (where an exclusive
* lock is irrelevant) or in cases where the current context already holds
* locks on all records which meet the specified criteria.
*
* @param fql
* Query string which defines the criteria which determine which records are deleted
* or updated.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param values
* Query substitution parameters, if any.
* @param entity
* Entity associated with the FQL statement, if any. Used to check whether a session
* flush is needed before execution.
* @param noUndo
* {@code true} if this operation is being performed on a NO-UNDO temp-table, else
* {@code false}.
*
* @return The number of entities deleted or updated.
*
* @throws PersistenceException
* if a problem occurs performing the bulk delete or update.
*/
@Override
public int deleteOrUpdate(String fql, boolean sharedDb, Object[] values, String entity, boolean noUndo)
throws PersistenceException
{
boolean trace = LOG.isLoggable(Level.FINEST);
long start = System.currentTimeMillis();
Context local = getContext(sharedDb);
int count = 0;
boolean inTx = local.beginTransaction(null);
try
{
Session session = local.getSessionNoCreate();
Query query = getQuery(local, fql, 0, 0, null, values, false);
count = query.executeUpdate(session);
if (inTx)
{
local.commit();
}
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, "[" + fql + "] count = " + count);
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "error performing bulk delete [" + fql + "]");
}
finally
{
if (trace)
{
long elapsed = System.currentTimeMillis() - start;
LOG.log(Level.FINEST, message("TIMING:DELETE [" + elapsed + "] " + fql));
}
}
return count;
}
/**
* Make sure a (new) transaction instance is created and stored in the local context. A new
* database session is created if one does not already exist in the current context.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #beginTransaction(boolean)} instead.
*
* @return {@code true} if a new transaction was begun;
* {@code false} if there was already an active transaction.
*
* @throws PersistenceException
* if there is an error beginning the new transaction.
*/
public boolean beginTransaction()
throws PersistenceException
{
return beginTransaction(null, PRIVATE_CTX);
}
/**
* Make sure a (new) transaction instance is created and stored in the local context. A new
* database session is created if one does not already exist in the current context.
*
* @return {@code true} if a new transaction was begun;
* {@code false} if there was already an active transaction.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @throws PersistenceException
* if there is an error beginning the new transaction.
*/
public boolean beginTransaction(boolean sharedDb)
throws PersistenceException
{
return beginTransaction(null, sharedDb);
}
/**
* Commit the current database transaction and close the current session. If the transaction cannot be
* committed, roll it back and raise an error.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #commit(boolean)} instead.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error committing the transaction.
*/
@Override
public void commit()
throws PersistenceException
{
commit(getContext(PRIVATE_CTX));
}
/**
* Commit the current database transaction and close the current session.
* If the transaction cannot be committed, roll it back and raise an error.
*
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error committing the transaction.
*/
@Override
public void commit(boolean sharedDb)
throws PersistenceException
{
commit(getContext(sharedDb));
}
/**
* Roll back the current database transaction and close the current
* session. If the transaction cannot be rolled back, raise an error.
*
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*
* @throws IllegalStateException
* if no transaction is active in the current session; this represents a programming error.
* @throws PersistenceException
* if there is an error rolling back the transaction.
*/
@Override
public void rollback(boolean sharedDb)
throws PersistenceException
{
rollback(getContext(sharedDb));
}
/**
* Roll back the current database transaction and close the current session. If the transaction cannot be
* rolled back, raise an error.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #rollback(boolean)} instead.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this represents a programming error.
* @throws PersistenceException
* if there is an error rolling back the transaction.
*/
@Override
public void rollback()
throws PersistenceException
{
rollback(getContext(PRIVATE_CTX));
}
/**
* Flush the current session, which synchronizes the in-memory state of DMOs to the database.
* <p>
* This request bypasses the current client context's setting as to whether flushing is enabled.
* <p>
* It is expected that this method will only be called within an active
* transaction. If no session is active, this method will raise an exception.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #flush(boolean)} instead.
*
* @throws IllegalStateException
* if no session currently is active.
* @throws PersistenceException
* if an error occurs during session flush.
*/
@Override
public void flush()
throws PersistenceException
{
Context local = getContext(PRIVATE_CTX);
Session session = local.getSessionNoCreate();
// Sanity.
if (session == null)
{
throw new PersistenceException("No active session to flush");
}
flush(local, session, false);
}
/**
* Flush the current session, which synchronizes the in-memory state of DMOs to the database.
* <p>
* This request bypasses the current client context's setting as to whether flushing is enabled.
* <p>
* It is expected that this method will only be called within an active
* transaction. If no session is active, this method will raise an exception.
*
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @throws IllegalStateException
* if no session currently is active.
* @throws PersistenceException
* if an error occurs during session flush.
*/
@Override
public void flush(boolean sharedDb)
throws PersistenceException
{
Context local = getContext(sharedDb);
Session session = local.getSessionNoCreate();
// Sanity.
if (session == null)
{
throw new PersistenceException("No active session to flush");
}
flush(local, session, false);
}
/**
* Indicate whether an explicit, database-level transaction currently is open.
* <p>
* <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
* effort to pick the right context, there are significant chances it WILL NOT work
* correctly in such environment. In single-tenant code it should work correctly, though.
* <p>To specify the user context for executing the query,
* use {@link #isTransactionOpen(boolean)} instead.
*
* @return {@code true} if open, else {@code false}.
*/
public boolean isTransactionOpen()
{
return getContext(PRIVATE_CTX).isTransactionOpen();
}
/**
* Indicate whether an explicit, database-level transaction currently is open.
*
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @return {@code true} if open, else {@code false}.
*/
public boolean isTransactionOpen(boolean sharedDb)
{
return getContext(sharedDb).isTransactionOpen();
}
/**
* Format the given message text by prepending a descriptor including the
* user's current context and the physical name of the database.
*
* @param message
* Core message
*
* @return Descriptor.
*/
public String message(String message)
{
Context[] ctxs = contexts.get(false);
Context ctx = (ctxs == null) ? null : ctxs[PRIVATE_IDX];
return "[" + Utils.describeContext() + "-->" +
(ctx != null ? ctx.tenant.database : defaultTenant.database) +
"] " + message;
}
/**
* Get the record lock context associated with the current client session. Although is method
* is public to allow access from other internal packages, it is not meant for use outside the
* <code>persist</code> package and its sub-packages.
*
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*
* @return Record lock context for the current session.
*/
public RecordLockContext getRecordLockContext(boolean sharedDb)
{
return getContext(sharedDb).getRecordLockContext();
}
/**
* Check if we can retrieve data lazily from the underlying database.
*
* @return {@code true} if we can use database cursors to iterate through the records
*/
public boolean supportsLazyQueryResultsMode()
{
return dialect.supportsLazyQueryResultsMode() && temporary;
}
/**
* Use the dialect to obtain the code page (character encoding) of this connection.
*
* @return the database's CP, if it can be retrieved in a dialect-specific manner.
*/
public String getCodepage()
{
if (codepage == NOT_DETECTED_YET) // compare objects directly!
{
try
{
// all physical databases of same logical persistence MUST be configured with same codepage
codepage = dialect.getCodepage(this.getSession(SHARED_CTX).getConnection());
}
catch (PersistenceException e)
{
codepage = null;
}
}
return codepage;
}
/**
* Use the dialect to obtain the collation of this connection.
*
* @return the database's collation, if it can be retrieved in a dialect-specific manner.
*/
public String getCollation()
{
if (collation == NOT_DETECTED_YET) // compare objects directly!
{
try
{
// all physical databases of same logical persistence MUST be configured with same collation
collation = dialect.getCollation(this.getSession(SHARED_CTX).getConnection());
}
catch (PersistenceException e)
{
collation = null;
}
}
return collation;
}
/**
* Check if we can skip the index server-validation.
*
* @return {@code true} If the dialect has support for unique index validation, meaning we can skip
* this process in the server.
*/
public boolean allowDBUniqueCheck()
{
return allowDBUniqueCheck;
}
/**
* Decide whether this persistence context is unsafe or not.
*
* @param value
* If true the persistence will run in unsafe mode and in safe mode if false.
* @param sharedDb
* In case of multi-tenant environment: is the buffer shared or private?
*/
public void setUnsafe(boolean value, boolean sharedDb)
{
getContext(sharedDb).setUnsafe(value);
}
/**
* Checks whether any tenants (except for default) were set for this persistence, in any context until
* this mment.
*
* @return {@code true} if at least one regular tenant was ever authenticated for this persistence.
*/
public boolean hasActiveTenants()
{
return isMultiTenancyEnabled && tenantData.size() > 1;
}
/**
* Checks whether any tenants (except for default) were set for this persistence, in any context.
*
* @return {@code true} if at least one regular tenant was authenticated for this persistence.
*/
public boolean hasActiveTenantInContext()
{
if (!hasActiveTenants())
{
return false; //quick out
}
Context[] ctxs = contexts.get();
return ctxs[PRIVATE_IDX] != null;
}
/**
* Initialize the helper objects to which this object delegates certain service requests.
*/
protected void initializeTenant(Tenant tenant)
{
try
{
tenant.lockManager.set(createLockManager(tenant.tenantId));
// if using embedded H2 dialect and not a permanent database, prepare it
if (temporary || tenant.database.isMeta() && !tenant.database.isTenant())
{
// tenant meta databases were prepared earlier, in MetadataManager.initMetaDb()
H2Helper.prepareDatabase(tenant.database);
}
else if (!tenant.database.isTenant() || !tenant.database.isMeta())
{
dialect.preparePermanentDatabase(tenant.database);
}
tenant.identityManager.set(createIdentityManager(tenant.tenantId));
}
catch (StopConditionException sce)
{
throw sce;
}
catch (Exception exc)
{
throw new RuntimeException(
"Error initializing persistence services for database '" + tenant.database.getName() + "'.",
exc);
}
}
/**
* Create and initialize the lock manager to be used with this instance of
* persistence services for the given database. The lock manager
* implementation to use is read from the directory. The default if no
* entry is found is the {@link InMemoryLockManager}.
*
* @param tenantId
* The tenant id.
*
* @return Lock manager instance.
*
* @throws IllegalAccessException
* if the <code>LockManager</code> concrete implementation's default constructor
* cannot be accessed.
* @throws java.lang.InstantiationException
* if the <code>LockManager</code> concrete implementation cannot be instantiated.
* @throws ClassNotFoundException
* if the <code>LockManager</code> concrete implementation class cannot be located.
* @throws IllegalArgumentException
* if lock metadata is used by the current application, but no lock table updater is
* found for the current database.
*/
protected LockManager<String> createLockManager(int tenantId)
throws ReflectiveOperationException
{
Tenant tenant = (tenantId == TenantManager.DEFAULT_TENANT_ID) ? defaultTenant
: getContext(PRIVATE_CTX).tenant;
if (tenant.database.isTemporary())
{
return null;
}
// retrieve fully qualified name of LockManager implementation class and instantiate it
String path = "database/" + tenant.database.getName() + "/p2j/lock_manager/class";
String className = Utils.getDirectoryNodeString(null, path, DEFAULT_LOCK_MANAGER, false);
LockManager<String> manager =
(LockManager<String>) Class.forName(className).getDeclaredConstructor().newInstance();
manager.setDatabase(tenant.database);
// register lock table updater with lock manager if the former exists
if (tenant.database.isPrimary())
{
// if a lock updater is found for the database, register it as a listener with the lock manager
tenant.lockTableUpdater = DatabaseManager.getLockTableUpdater(tenant.database);
if (tenant.lockTableUpdater != null)
{
manager.setLockListener(tenant.lockTableUpdater);
}
}
if (LOG.isLoggable(Level.CONFIG))
{
LOG.config(message("lock manager initialized [" + className + "]"));
}
return manager;
}
/**
* Create and initialize the identity manager to be used with this
* instance of persistence services for the given database. The identity
* manager implementation to use is read from the directory. The default
* if no entry is found is {@link #DEFAULT_IDENTITY_MANAGER}.
*
* @param tenantId
* The tenant id.
*
* @return Identity manager instance or <code>null</code> if given database is temporary.
*
* @throws IllegalAccessException
* if the <code>IdentityManager</code> concrete implementation's
* default constructor cannot be accessed.
* @throws java.lang.InstantiationException
* if the <code>IdentityManager</code> concrete implementation
* cannot be instantiated.
* @throws ClassNotFoundException
* if the <code>IdentityManager</code> concrete implementation
* class cannot be located.
*/
protected IdentityManager createIdentityManager(int tenantId)
throws ReflectiveOperationException
{
Tenant tenant = (tenantId == TenantManager.DEFAULT_TENANT_ID) ? defaultTenant
: getContext(PRIVATE_CTX).tenant;
if (tenant.database.isTemporary())
{
return null;
}
String className = DEFAULT_IDENTITY_MANAGER;
boolean usePool = false;
if (tenant.database.isPrimary())
{
// retrieve fully qualified name of IdentityManager implementation class and instantiate
String dbName = tenant.database.getName();
String path = "database/" + dbName + "/p2j/identity_manager/class";
className = Utils.getDirectoryNodeString(null, path, DEFAULT_IDENTITY_MANAGER, false);
// check whether use of identity pool is forced by directory
path = String.format("database/%s/p2j/identity_manager/pool", dbName);
usePool = Utils.getDirectoryNodeBoolean(null, path, false, false);
}
IdentityPoolManager mgr = (IdentityPoolManager) Class.forName(className).getDeclaredConstructor().newInstance();
mgr.setPersistence(this);
IdentityPool identityPool = usePool ? new IdentityPool(tenant.database) : null;
mgr.setIdentityPool(identityPool);
if (LOG.isLoggable(Level.CONFIG))
{
LOG.config(message("identity manager initialized [" + className + "]"));
}
return mgr;
}
/**
* Determine whether this persistence object is for a temporary table database.
*
* @return {@code true} if temporary, else {@code false}.
*/
public boolean isTemporary()
{
return temporary;
}
/**
* Begin a new transaction and store the instance in the local context. A new database
* session is created if one does not already exist in the current context.
*
* @return {@code true} if a new transaction was begun;
* {@code false} if there was already an active transaction.
*
* @throws PersistenceException
* if there is an error beginning the new transaction.
*/
boolean beginTransaction(SavepointManager savepointManager, boolean sharedDb)
throws PersistenceException
{
return beginTransaction(getContext(sharedDb), savepointManager);
}
/**
* Begin a new transaction and store the instance in the local context. A new database
* session is created if one does not already exist in the current context.
*
* @param local
* User context.
*
* @return {@code true} if a new transaction was begun;
* {@code false} if there was already an active transaction.
*
* @throws PersistenceException
* if there is an error beginning the new transaction.
*/
boolean beginTransaction(Context local, SavepointManager savepointManager)
throws PersistenceException
{
if (!local.beginTransaction(savepointManager))
{
return false;
}
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, message("beginning transaction"));
}
return true;
}
/**
* Commit the current database transaction and close the current session.
* If the transaction cannot be committed, roll it back and raise an error.
*
* @param local
* User context.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error committing the transaction.
*/
void commit(Context local)
throws PersistenceException
{
local.commit();
}
/**
* Roll back the current database transaction and close the current
* session. If the transaction cannot be rolled back, raise an error.
*
* @param local
* User context.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error rolling back the transaction.
*/
void rollback(Context local)
throws PersistenceException
{
try
{
local.rollback(false);
}
catch (IllegalStateException exc)
{
throw new PersistenceException(exc);
}
}
/***
* Execute a batch SQL statement, while the argument supplier returns non-null values.
*
* @param sql
* The SQL statement to execute.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param getArgs
* The argument supplier.
*/
@Override
public void executeSQLBatch(String sql, boolean sharedDb, Supplier<Object[]> getArgs)
throws PersistenceException
{
Context local = getContext(sharedDb);
long start = 0L;
boolean trace = LOG.isLoggable(Level.FINEST);
if (trace)
{
start = System.currentTimeMillis();
}
boolean inTx = local.beginTransaction(null);
try
{
Session session = local.getSession();
try (PreparedStatement stmt = session.getConnection().prepareStatement(sql))
{
int batchSize = 50;
int i = 0;
do
{
Object[] args = getArgs.get();
if (args == null)
{
break;
}
for (int k = 0; k < args.length; k++)
{
stmt.setObject(k + 1, args[k]);
}
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "FWD ORM:\n" + stmt);
}
stmt.addBatch();
if (++i % batchSize == 0)
{
SQLExecutor.getInstance().execute(session.getDatabase(),
sql,
PreparedStatement::executeBatch,
stmt,
batchSize);
}
}
while (true);
if (i % batchSize != 0)
{
stmt.executeBatch();
}
}
if (inTx)
{
local.commit();
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "Error executing SQL statement: " + sql);
}
catch (SQLException e)
{
PersistenceException pe = new PersistenceException("Failed to execute SQL statement.", e);
handleException(local, pe, "Error executing SQL statement: " + sql);
}
finally
{
if (trace)
{
long elapsed = System.currentTimeMillis() - start;
LOG.log(Level.FINEST, message("TIMING:SQLB [" + elapsed + "] " + sql));
}
}
}
/**
* Execute an arbitrary list of standard JDBC statements in batch.
*
* @param local
* Persistence context.
* @param sql
* List of SQL statements to execute in batch.
* @param noFlush
* If {@code true}, flushing is bypassed.
*
* @throws PersistenceException
* if any error occurs executing the statements.
*/
@Override
public void executeSQLBatch(Context local, List<String> sql, boolean noFlush)
throws PersistenceException
{
if (sql.isEmpty())
{
return;
}
long start = 0L;
boolean trace = LOG.isLoggable(Level.FINEST);
if (trace)
{
start = System.currentTimeMillis();
}
boolean inTx = local.beginTransaction(null);
try
{
Session session = local.getSession();
try (Statement stmt = session.getConnection().createStatement())
{
int batchSize = 50;
int i = 0;
for (String s : sql)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "FWD ORM:\n" + s);
}
stmt.addBatch(s);
if (++i % batchSize == 0)
{
SQLExecutor.getInstance().execute(session.getDatabase(),
s,
Statement::executeBatch,
stmt,
batchSize);
}
}
if (i % batchSize != 0)
{
stmt.executeBatch();
}
}
if (inTx)
{
local.commit();
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "Error executing SQL statement: " + sql);
}
catch (SQLException e)
{
PersistenceException pe = new PersistenceException("Failed to execute SQL statement.", e);
handleException(local, pe, "Error executing SQL statement: " + sql);
}
finally
{
if (trace)
{
long elapsed = System.currentTimeMillis() - start;
LOG.log(Level.FINEST, message("TIMING:SQLB [" + elapsed + "] " + sql));
}
}
}
/**
* Execute an arbitrary SQL statement which does not produce a result set.
*
* @param sql
* SQL statement text.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
* @param args
* The statement arguments.
* @param noUndo
* If {@code true}, the update is on a NO-UNDO temp-table.
*
* @return The number of entities updated or deleted, or {@code -1} if the statement
* did not complete normally.
*
* @throws PersistenceException
* if any error occurs executing the statement.
*/
@Override
public int executeSQL(String sql, boolean sharedDb, Object[] args, boolean noUndo)
throws PersistenceException
{
long start = 0L;
boolean trace = LOG.isLoggable(Level.FINEST);
if (trace)
{
start = System.currentTimeMillis();
}
Context local = getContext(sharedDb);
int rc = -1;
boolean inTx = local.beginTransaction(null);
try
{
Session session = local.getSession();
SQLQuery query = Session.createSQLQuery(sql);
if (args != null)
{
int len = args.length;
for (int i = 0; i < len; i++)
{
query.setParameter(i, args[i]);
}
}
rc = query.executeUpdate(session);
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, "[" + sql + "] rc = " + rc);
}
if (inTx)
{
local.commit(); // commit only the local transaction,
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "Error executing SQL statement: " + sql);
}
finally
{
if (trace)
{
long elapsed = System.currentTimeMillis() - start;
LOG.log(Level.FINEST, message("TIMING:SQL [" + elapsed + "] " + sql));
}
}
return rc;
}
/**
* If this object is associated with a metadata database, set this object's lock table updater
* to be the same as that of the <code>Persistence</code> instance associated with the given,
* primary database.
*
* @param database
* Primary database presumably associated with the metadata database represented by
* this object.
*
* @throws PersistenceException
* if this object is not associated with a metadata database or the given database is
* not a primary database.
*/
void shareLockTableUpdater(Database database)
throws PersistenceException
{
Tenant tenant = (tenantData.size() == 1) ? defaultTenant
: getContext(!database.isTenant()).tenant;
if (!tenant.database.isMeta() && database.isPrimary())
{
throw new PersistenceException("Invalid lock updater share request");
}
Persistence pPrim = PersistenceFactory.getInstance(database);
tenant.lockTableUpdater = pPrim.getContext(!database.isTenant()).tenant.lockTableUpdater;
}
/**
* Evict the given DMO from the current session immediately. If there is no existing session,
* this method is a no-op.
*
* @param local
* Persistence context.
* @param dmo
* Data model object to be evicted.
*/
void evict(Persistence.Context local, Record dmo)
{
Session session = local.getSessionNoCreate();
if (session != null)
{
session.evict(dmo);
}
}
/**
* Evict a collection of DMOs from the current ORM session. If there is no existing
* session, this method is a no-op.
*
* @param evictees
* Collection of DMOs to be evicted.
*/
void evict(Collection<Record> evictees)
{
Context[] ctxs = contexts.get();
Context def = ctxs[SHARED_IDX];
Context ten = ctxs[PRIVATE_IDX];
Session defSession = def.getSessionNoCreate();
Session tenSession = (ten == null) ? defSession : ten.getSessionNoCreate();
if (defSession != null || tenSession != null)
{
for (Record dmo : evictees)
{
if (dmo._getRecordMeta().getDmoMeta().multiTenant)
{
if (tenSession != null)
{
tenSession.evict(dmo);
}
}
else
{
if (defSession != null)
{
defSession.evict(dmo);
}
}
}
}
}
/**
* Register a <code>SessionListener</code> to receive session life
* cycle event notifications. The listener will be deregistered
* according to the following rules at the earlier of:
* <ul>
* <li>the point at which it is collected by the garbage collector
* (once business logic no longer strongly references it); or
* <li>the end of the scope indicated by the <code>scope</code>
* parameter:
* <ul>
* <li><code>ENCLOSING_EXTERNAL_SCOPE</code>
* <li><code>CURRENT_SCOPE</code>
* <li><code>NEXT_EXTERNAL_SCOPE</code>
* </ul>
* </ul>
*
* @param listener
* Session listener to be registered.
* @param scope
* {@code SessionListener.Scope} enum indicating the scope to which this listener should be
* registered. When the scope ends, the listener is deregistered as described above.
* @param sharedDb
* {@code true} if the buffers are shared (not tenant private).
*/
void registerSessionListener(SessionListener listener, SessionListener.Scope scope, boolean sharedDb)
{
Context local = getContext(sharedDb);
local.registerSessionListener(listener, scope);
}
/**
* Deregister a session listener when it no longer needs to receive session life cycle events.
*
* @param listener
* Listener to deregister.
*/
void deregisterSessionListener(SessionListener listener)
{
Context[] ctxs = contexts.get();
ctxs[SHARED_IDX].deregisterSessionListener(listener);
if (ctxs[PRIVATE_IDX] != null)
{
ctxs[PRIVATE_IDX].deregisterSessionListener(listener);
}
}
/**
* Process an error condition in which a unique result should have been
* found, but multiple results occurred.
*
* @param buffer
* Record buffer associated with the failed query.
* @param exc
* Optional root cause exception.
*
* @throws PersistenceException
* always; indicates the nature of the error.
*/
void uniqueResultViolation(RecordBuffer buffer, Exception exc)
throws PersistenceException
{
int errnum = 3166;
buffer.setAmbiguous(true);
String msg = ErrorManager.buildErrorText(errnum,
"More than one " +
buffer.getLegacyName() +
" records found by a unique FIND",
false);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, message(msg), exc);
}
// NOTE: do not add the UniqueResultException as cause for the new exception because it will leak
// into UI (see ErrorManager.compileErrorEntries())
throw new PersistenceException(msg, errnum);
}
/**
* Flush the current session, which synchronizes the in-memory state of
* DMOs to the database. Optionally, the session is cleared as well.
* This request bypasses the current client context's setting as to
* whether flushing is enabled.
*
* @param local
* Context-local state.
* @param session
* ORM session
* @param clear
* <code>true</code> to clear the session (used during bulk
* inserts or updates to prevent a progressive memory leak) after
* the flush.
*
* @throws PersistenceException
* if an error occurs during session flush.
*/
/* private */ void flush(Context local, Session session, boolean clear)
throws PersistenceException
{
try
{
if (clear)
{
session.clear();
}
}
catch (PersistenceException exc)
{
handleException(local, exc, "error flushing and clearing session");
}
}
/**
* Perform special handling on query substitution parameters to avoid
* errors during query execution. This entails:
* <ul>
* <li>Trimming all trailing spaces, tabs, line feeds, and carriage
* returns from any parameter of type <code>character</code>. Any
* parameter affected is replaced with a new <code>character</code>
* instance at the same index position within <code>args</code>.
* <li>Clipping out of bounds date values to the range supported by the
* backing database's implementation of a date data type. Any
* parameter affected is replaced with a new <code>date</code>
* instance at the same index position within <code>args</code>.
* </ul>
* <p>
* Note: this method has special processing for <code>null</code> bytes
* embedded in <code>character</code> variables. The text of the variable
* is truncated starting at the first embedded <code>null</code> byte.
* This means that the resulting substitution parameters are guaranteed
* to never include <code>null</code> bytes.
*
* @param args
* Array of query substitution parameters or <code>null</code> if
* none.
*/
private void preprocessQueryParameters(Object[] args)
{
if (args != null)
{
int len = args.length;
for (int i = 0; i < len; i++)
{
args[i] = preprocessQueryParameter(args[i], dialect);
}
}
}
/**
* Get or create a query for the given FQL query string. A cached query is used if it exists.
*
* @param local
* Context-local state.
* @param fql
* FQL query string.
* @param maxResults
* The maximum number of results to be returned by the query. If this value is
* non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive,
* an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param values
* Substitution values for the query. If none, this should be an empty array.
*
* @return FQL query object.
*
* @throws PersistenceException
* if there was an error creating an ORM session.
*/
private Query getQuery(Context local,
String fql,
int maxResults,
int startOffset,
DataRelation relation,
Object[] values)
throws PersistenceException
{
return getQuery(local, fql, maxResults, startOffset, relation, values, false);
}
/**
* Get or create a query for the given FQL query string. A cached query is used if it exists.
*
* @param local
* Context-local state.
* @param fql
* FQL query string.
* @param maxResults
* The maximum number of results to be returned by the query. If this value is
* non-positive, no upper limit is applied.
* @param startOffset
* The 0-based offset of the first record to retrieve. If this value is non-positive,
* an offset of 0 is used by default.
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param values
* Substitution values for the query. If none, this should be an empty array.
* @param lazyMode
* Flag that indicates whether the query should be executed in a lazy manner.
*
* @return FQL query object.
*
* @throws PersistenceException
* if there was an error creating an ORM session.
*/
private Query getQuery(Context local,
String fql,
int maxResults,
int startOffset,
DataRelation relation,
Object[] values,
boolean lazyMode)
throws PersistenceException
{
Query query = local.getQuery(fql, maxResults, startOffset, relation, lazyMode);
// Bind parameters, if any. It is OK to do this with cached queries, because each use is
// guaranteed to reset the parameters.
if (values != null && values.length > 0)
{
preprocessQueryParameters(values);
// query.setParameters(values, types);
// This is an workaround for setting up named parameters. It was needed because
// Hibernate cannot handle multi-column values with positional parameters.
// Even this could work for all cases, the implementation was only tested for
// simple cases CompositeCustomType that were needed for datetime-tz.
// The code will be eliminated anyway when named parameters will replace
// positional parameters globally.
int posIndex = 0;
for (int k = 0; k < values.length; k++)
{
/*
if (types[k] instanceof org.hibernate.type.CompositeCustomType)
{
query.setParameter("px" + (++posIndex), values[k], types[k]);
}
else
*/
{
// query.setParameter(Integer.toString(k), values[k], types[k]);
query.setParameter(k, values[k]);
}
}
}
return query;
}
/**
* Detects and return the tenant context based on the list of entities passed as argument. If
* {@code entities} is {@code null} or does not contain any entity, or if entities from different
* physical databases are identified, the tenant database context is returned if a tenant is active.
* Otherwise, the permanent/no-tenant database context is returned.
* <p>
* In case of lack of information or a conflict (that is, both shared and private tables are detected), a
* warning will be issued.
*
* @param entities
* An array of {@code Class} objects which represent the tables involved in a query.
*
* @return The multi-tenant context which should be used for executing the query.
*/
private Context getContext(Class<? extends Record>[] entities)
{
if (!hasActiveTenants())
{
// quick out: not a multitenant database (_temp, meta) or no tenant set yet
return getContext(SHARED_CTX);
}
int ctxType = -1;
if (entities != null)
{
for (Class<? extends Record> entity : entities)
{
DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity);
if (dmoInfo.multiTenant)
{
if (ctxType == SHARED_IDX)
{
ctxType = -2;
break;
}
ctxType = PRIVATE_IDX;
}
else // !dmoInfo.multiTenant
{
if (ctxType == PRIVATE_IDX)
{
ctxType = -2;
break;
}
ctxType = SHARED_IDX;
}
}
}
if (ctxType == -1)
{
LOG.log(Level.INFO, "MTCtx: No entities provided!");
}
else if (ctxType == -2)
{
LOG.log(Level.WARNING, "MTCtx: Failed TENANT context detection: collision detected!");
}
return getContext(ctxType == SHARED_IDX);
}
/**
* Raise a STOP condition, if the given exception represents a catastrophic, JDBC failure;
* else throw a <code>PersistenceException</code>.
*
* @param message
* Error text for the exception.
* @param exc
* A persistence exception which may or may not represent an unrecoverable database error.
*
* @throws StopConditionException
* if the error is unrecoverable.
* @throws PersistenceException
* if the error is likely recoverable.
*/
private void handleException(String message, PersistenceException exc)
throws PersistenceException
{
if (exc instanceof UdfException) // TODO: temporary, need more robust inspection
{
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, message);
}
throw new StopConditionException(message(message), exc);
}
else if (exc.getCause() instanceof SQLException) // TODO: temporary, need more robust inspection
{
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, message, exc);
}
if (!dialect.isRecoverableError(exc))
{
throw new StopConditionException(message(message), exc);
}
}
throw new PersistenceException(message(message), exc);
}
/**
* Provides a template for handling <link>PersistenceException</link> exceptions. Depending on context
* a more suitable exception will be thrown. A rollback for the local context will also be initiated and
* a log message will be written.
*
* @param local
* The client context.
* @param exc
* A persistence exception which may or may not represent an unrecoverable database error.
* @param message
* The message which will be written as a log. If null no message will be displayed.
*
* @throws StopConditionException
* if the error is unrecoverable.
* @throws PersistenceException
* if the error is likely recoverable.
*/
private void handleException(Context local, PersistenceException exc, String message)
throws PersistenceException
{
if (!local.isUnsafe())
{
DBUtils.handleException(local.tenant.database, exc);
local.closeSession(true);
String msg = "";
if (message != null)
{
msg = message(message);
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, msg);
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Details:", exc);
}
}
}
handleException(msg, exc);
}
else
{
throw exc;
}
}
/**
* Returns a short description of this object.
*
* @return a short description of this object.
*/
@Override
public String toString()
{
return "Persistence{" + dialect.getJdbcDatabaseType() + ": " + getDatabase(Persistence.PRIVATE_CTX) + "}";
}
/**
* When a tenant id is changed, we need to re-open the session, but taking into account
* the new tenant's name.
*
* @param tenantName
* The new tenant's name.
* @param ldbName
* The logical name of the database that the tenant will use in this context.
*/
@Override
public void tenantChanged(String tenantName, String ldbName)
{
Database defaultDb = defaultTenant.database;
if (!isMultiTenancyEnabled || !ldbName.equalsIgnoreCase(defaultDb.getName()))
{
return; // quick out, this should not happen, anyway.
}
if (tenantName == null || TenantManager.DEFAULT_TENANT_NAME.equalsIgnoreCase(tenantName))
{
Context[] ctxs = contexts.get();
Context tCtx = ctxs[PRIVATE_IDX];
tCtx.cleanup();
ctxs[PRIVATE_IDX] = null;
return;
}
Database mtDb = TenantManager.getDatabase(tenantName, ldbName, false, defaultDb.getType());
if (mtDb == null)
{
LOG.log(Level.WARNING,
"Failed to obtain the physical database for tenant '" + tenantName + "'.\n" +
"Tenant will not be changed!");
return;
}
Tenant tenant = tenantData.computeIfAbsent(tenantName.toLowerCase(), s -> {
int tenantId = TenantManager.getTenant(tenantName, null).id();
Tenant t = new Tenant(mtDb, tenantId, tenantName);
initializeTenant(t); // re-initialize database to fit the new tenant
return t;
});
Context[] ctxs = contexts.get();
Context tCtx;
boolean isNew = false;
if (ctxs[PRIVATE_IDX] == null)
{
// create the pure-tenant context, if it does not exist
tCtx = ctxs[PRIVATE_IDX] = new Context(tenant);
isNew = true;
}
else
{
tCtx = ctxs[PRIVATE_IDX];
}
// do a bit of cleanup, preparing for the tenant change
try
{
// invalidate all buffers to avoid leaking from one tenant database to another using the buffers
// process only the multi-tenant buffers, the shared buffers should not be affected
Iterator<RecordBuffer> it = tCtx.bufferManager.getBuffersOf(Persistence.this, true);
while (it.hasNext())
{
RecordBuffer buffer = it.next();
buffer.release(true);
buffer.updateTenant(tCtx);
}
if (!isNew)
{
tCtx.closeSessionImpl(false, false);
tCtx.factory.expire();
tCtx.initialize(tenant);
}
}
catch (PersistenceException e)
{
LOG.log(Level.WARNING, "Failed to re-open the new context for existing buffers of " + tenant);
}
}
/**
* A hash key for a unique combination of FQL, with the existence of lack of maximum results,
* and starting row offset options. The actual values for paging are not cached, we need to
* know only the query form and the actual values are filled when object is used.
*/
private static class QueryKey
{
/** FQL statement */
private final String fql;
/** Does the query was generated with maximum results option? */
private final boolean hasMaxResults;
/** Does the query was generated with starting row offset option? */
private final boolean hasStartOffset;
/** The where string of the recursive DATA-RELATION the query is to FILL for */
private final String whereStr;
/** Precalculated hash code */
private final int hash;
/** Check if the query should be executed in lazy mode (sensitive to change in real-time) */
private final boolean lazyMode;
/**
* Constructor.
*
* @param hql
* FQL statement
* @param maxResults
* Maximum results.
* @param startOffset
* Starting row offset.
*/
QueryKey(String hql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
{
this.fql = hql;
this.hasMaxResults = maxResults > 0;
this.hasStartOffset = startOffset > 0;
this.whereStr = relation == null ? "" : relation.getWhereString().toJavaType();
this.lazyMode = lazyMode;
int result = 17;
result = 37 * result + hql.hashCode();
result = 37 * result + (hasMaxResults ? 1 : 0) + (hasStartOffset ? 2 : 0);
result = 37 * result + whereStr.hashCode();
result = 37 * result + (lazyMode ? 1 : 0);
this.hash = result;
}
/**
* Test for equivalence with another object.
*
* @param o
* Another <code>ColumnKey</code> instance.
*
* @return <code>true</code> if <code>o</code> has same internal
* state as this object; else <code>false</code>.
*/
@Override
public boolean equals(Object o)
{
if (!(o instanceof QueryKey))
{
return false;
}
QueryKey that = (QueryKey) o;
return this.hash == that.hash &&
this.hasMaxResults == that.hasMaxResults &&
this.hasStartOffset == that.hasStartOffset &&
this.fql.equals(that.fql) &&
this.whereStr.equals(that.whereStr) &&
this.lazyMode == that.lazyMode;
}
/**
* A hash code implementation consistent with our overridden {@link #equals(Object)} method.
*
* @return Hash code.
*/
@Override
public int hashCode()
{
return hash;
}
/**
* Get string representation of this object.
*
* @return String representation.
*/
@Override
public String toString()
{
return fql + "[" + hasMaxResults + ":" + hasStartOffset + "];" + whereStr;
}
}
/**
* A storage and work area for context-local variables used by the
* enclosing class. One instance of this class is created for each
* client context. It is used to:
* <ul>
* <li>create and cache query objects;
* <li>create, store and close the active database session;
* <li>manage the active database transaction.
* </ul>
* <p>
* Special care is taken to not close a database session while other
* entities are dependent upon it for services.
*/
public class Context
{
/** Cache of FQL strings, with or without max results and start offsets, to queries. */
private ExpiryCache<QueryKey, Query> staticQueryCache;
/** Context-local buffer manager */
private final BufferManager bufferManager = BufferManager.get();
/** The current tenant information. */
private Tenant tenant;
/** Cache for FIND results. */
private FastFindCache ffCache;
/** Object which manages newly created records */
private RecordNursery nursery;
/** Session listener objects */
private final WeakHashMap<SessionListener, Finalizable> sessionListeners = new WeakHashMap<>();
/** Active database session, if any */
private Session session = null;
/** Reference count of resources dependent upon current session */
private int sessionUseCount = 0;
/** Flag to prevent nested session close attempts */
private boolean closingSession = false;
/** Count of open database sessions in this context */
private volatile int openSessionCount = 0;
/** Optional, dialect-specific, context-local data */
private final Object dialectData = dialect.createContext(defaultTenant.database);
/** rereadnolock command-line parameter, by default deactivated */
private boolean rereadNoLock = false;
/** Enable tracing transaction starts */
private boolean traceTx = false;
/** Throwable which holds stack trace where last transaction began */
private Throwable beginTxTrace = null;
/** A factory that allows a session to be reclaimed */
private final SessionFactory factory = new SessionFactory();
/** Flag used to indicate if default exception handling is used. */
private boolean unsafe = false;
/**
* The default constructor reads rereadNoLock from directory.
*
* @param t
* The tenant for this context.
*/
private Context(Tenant t)
{
initialize(t);
// the '-rereadnolock' startup parameter was introduced in Progress 8.3B'
// only needed for permanent tables, not temp-tables
rereadNoLock = !temporary && SessionUtils._startupParameters().getRereadNoLock();
// if the database is multi-tenant, register the persistence only once, when the default context is
// created
if (isMultiTenancyEnabled && t.tenantId == TenantManager.DEFAULT_TENANT_ID)
{
ConnectionManager cm = ConnectionManager.get();
String ldbName = cm.getLDBName(tenant.database);
if (ldbName == null)
{
ldbName = tenant.database.getName().toLowerCase();
}
cm.registerTenantListener(Persistence.this, ldbName);
}
}
/**
* Initialization of the context. The query cache, fast-find cache and the record nursery are
* re-instantiated with new objects. This method is called from each constructor, but also when the
* tenant is changed.
*
* @param t
* The tenant for this context.
*/
void initialize(Tenant t)
{
this.tenant = t;
this.staticQueryCache = CacheManager.createLRUCache(Persistence.class, 1024);
this.ffCache = FastFindCache.getInstance(isTemporary(), tenant.database);
this.nursery = new RecordNursery(ffCache);
}
/**
* Get the record lock context associated with the current client
* session.
*
* @return Record lock context for the current session.
*/
RecordLockContext getRecordLockContext()
{
RecordLockContext rlc = tenant.lockContext.get();
if (rlc == null)
{
tenant.lockContext.set(rlc = RecordLockContext.get(
Persistence.this, bufferManager, tenant.tenantId == TenantManager.DEFAULT_TENANT_ID));
}
return rlc;
}
/**
* Get the {@code Persistence} object associated with this context.
*
* @return Persistence object.
*/
Persistence getPersistence()
{
return Persistence.this;
}
/**
* Increment the session use count, indicating the addition of a dependency on the open
* session. As long as there are dependencies on the session, it may not close.
*/
void useSession()
{
sessionUseCount++;
// LOG.finer("--> (" + sessionUseCount + ") " + database);
}
/**
* Decrement the session use count, indicating the removal of a dependency on the open
* session. As long as there are dependencies on the session, it may not close.
*/
void releaseSession()
{
sessionUseCount--;
// LOG.finer("<-- (" + sessionUseCount + ") " + database);
}
/**
* Indicate whether there is an open session and if so, whether it can be closed. An open
* session may only be closed if there are no dependencies on it.
*
* @return {@code true} if there is an open session which may be closed; otherwise {@code false}.
*/
boolean canCloseSession()
{
return session != null && sessionUseCount < 1 && session.isOpen();
}
/**
* Get a cached query for the given combination of FQL query string, maximum result rows,
* and starting row offset, creating and caching a new query if no such query already is
* cached. The cache is fixed size. When it is full and a new query needs to be added, the
* least recently used query is removed.
*
* @param fql
* FQL query string.
* @param maxResults
* The maximum number of results to be returned.
* @param startOffset
* Zero-based offset of first result to be returned (undefined results unless
* {@code fql} includes an {@code order by} phrase).
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
*
* @return A {@code Query} object for the requested {@code fql}.
*
* @throws PersistenceException
* if there was an error creating an ORM session.
*/
Query getQuery(String fql, int maxResults, int startOffset, DataRelation relation)
throws PersistenceException
{
return getQuery(fql, maxResults, startOffset, relation, false);
}
/**
* Get a cached query for the given combination of FQL query string, maximum result rows,
* and starting row offset, creating and caching a new query if no such query already is
* cached. The cache is fixed size. When it is full and a new query needs to be added, the
* least recently used query is removed.
*
* @param fql
* FQL query string.
* @param maxResults
* The maximum number of results to be returned.
* @param startOffset
* Zero-based offset of first result to be returned (undefined results unless
* {@code fql} includes an {@code order by} phrase).
* @param relation
* The recursive DATA-RELATION the query is to FILL for.
* @param lazyMode
* Flag that indicates whether the query should be executed in a lazy manner.
*
* @return A {@code Query} object for the requested {@code fql}.
*
* @throws PersistenceException
* if there was an error creating an ORM session.
*/
Query getQuery(String fql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
throws PersistenceException
{
Query query = null;
fql = fql.trim(); // just in case
// TODO: use the dialect also? I.e: is it possible to have different SQL for same FQL
// if different dialects are used?
QueryKey key = new QueryKey(fql, maxResults, startOffset, relation, lazyMode);
synchronized (staticQueryCache)
{
query = staticQueryCache.get(key);
if (query == null)
{
query = Session.createQuery(q ->
{
synchronized (staticQueryCache)
{
// NOTE: the FQL string is not converted to SQL at this time. The conversion
// is performed once, lazily, when the first navigation is required by a
// call of list(), scroll(), or uniqueResult() on the new query
staticQueryCache.put(key, q);
}
},
fql);
}
}
// update [maxResults] and [startOffset] because the cached query might have set
// an incorrect value from a previous usage
query.setMaxResults((maxResults > 0) ? maxResults : -1);
query.setFirstResult(Math.max(startOffset, 0));
query.setDataRelation(relation);
query.setLazyMode(lazyMode);
return query;
}
/**
* Commit the current database transaction. If the transaction cannot be committed, roll it
* back and raise an error.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error committing the transaction.
*/
void commit()
throws PersistenceException
{
if (!isTransactionOpen())
{
throw new IllegalStateException(message("no current transaction available to commit"));
}
boolean explicitRollback = false;
try
{
if (!temporary && !sessionListeners.isEmpty())
{
notifySessionEvent(SessionListener.Event.TRANSACTION_COMMITTING);
}
session.commit();
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, message(" transaction committed"));
}
postTransaction(false);
}
catch (PersistenceException exc)
{
if (isUnsafe())
{
DBUtils.handleException(session.getDatabase(), exc);
if (session.inTransaction())
{
try
{
explicitRollback = true;
rollback(true);
}
catch (PersistenceException exc1)
{
// don't need to throw an exception here; this would hide the commit exception,
// so just log it
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, message("rollback error"), exc1);
}
}
}
handleException("unable to commit transaction", exc);
}
else
{
throw exc;
}
}
finally
{
if (!explicitRollback)
{
// if there was an explicit rollback, we will have decremented the session in that logic;
// otherwise, do it here
releaseSession();
}
}
}
/**
* Roll back the current transaction and possibly close the current session. If the
* transaction cannot be rolled back, raise an error.
*
* @param error
* Indicates whether the rollback is due to an error.
*
* @throws IllegalStateException
* if no transaction is active in the current session; this
* represents a programming error.
* @throws PersistenceException
* if there is an error rolling back the transaction.
*/
void rollback(boolean error)
throws PersistenceException
{
try
{
if (!isTransactionOpen())
{
throw new IllegalStateException(message("no current transaction available to rollback"));
}
session.rollback();
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, message("transaction rolled back"));
}
}
catch (PersistenceException exc)
{
error = true;
handleException(this, exc, "unable to rollback current transaction");
}
finally
{
releaseSession();
postTransaction(true);
}
}
/**
* Set enable mode for transaction tracing.
*
* @param enable
* <code>true</code> to enable; <code>false</code> to disable.
*/
void enableTransactionTrace(boolean enable)
{
this.traceTx = enable;
}
/**
* Begin a new database transaction and remember that it is open, for related calls. If a
* transaction already exists, return {@code false}, and do not begin a new one. A new
* database session is created if one does not already exist in the current context.
*
* @param savepointManager
* A savepoint manager to install in the session while the transaction is active.
* May be {@code null} for ad-hoc transactions, but must not be {@code null} when
* the databbase transaction is being opened in association with a legacy
* application transaction.
*
* @return {@code true} if a new database transaction was opened successfully;
* {@code false} if a transaction already exists.
*
* @throws PersistenceException
* if there is an error beginning the new transaction.
*/
boolean beginTransaction(SavepointManager savepointManager)
throws PersistenceException
{
if (getSession().inTransaction())
{
return false;
}
boolean success = false;
try
{
Session session = getSession();
success = session.beginTransaction(savepointManager);
if (session.inTransaction() && traceTx)
{
beginTxTrace = new Throwable();
}
useSession();
}
catch (PersistenceException exc)
{
success = false;
handleException(this, exc, "error beginning database transaction");
}
return success;
}
/**
* Perform post-processing and cleanup after a transaction commit or rollback.
*
* @param rollback
* Indicates whether the transaction is being ended after a rollback (as opposed
* to a commit).
*
* @throws PersistenceException
* if an error occurs performing post-transaction processing, if any.
*/
void postTransaction(boolean rollback)
throws PersistenceException
{
try
{
// give the dialect a chance to perform post-transaction processing
dialect.postTransaction(Persistence.this, dialectData, rollback);
}
finally
{
beginTxTrace = null;
}
}
/**
* Retrieve trace information about where the current transaction, if any, was started.
* FINER level logging must be enabled, or this method always will return <code>null</code>.
*
* @return <code>Throwable</code> containing stack trace from last transaction start, or
* <code>null</code>.
*/
Throwable getBeginTxTrace()
{
return beginTxTrace;
}
/**
* Register a <code>SessionListener</code> to receive session life
* cycle event notifications. The listener will be deregistered
* according to the following rules at the earlier of:
* <ul>
* <li>the point at which it is collected by the garbage collector
* (once business logic no longer strongly references it); or
* <li>the end of the scope indicated by the <code>scope</code>
* parameter:
* <ul>
* <li><code>ENCLOSING_EXTERNAL_SCOPE</code>
* <li><code>CURRENT_SCOPE</code>
* <li><code>NEXT_EXTERNAL_SCOPE</code>
* </ul>
* </ul>
*
* @param listener
* Session listener to be registered.
* @param scope
* <code>SessionListener.Scope</code> enum indicating the scope
* to which this listener should be registered. When the
* scope ends, the listener is deregistered as described above.
*/
void registerSessionListener(final SessionListener listener, SessionListener.Scope scope)
{
if (SessionListener.Scope.NONE.equals(scope))
{
// Store listener as a key in the map, so it may be released when no
// longer referenced elsewhere.
sessionListeners.put(listener, null);
// Never deregister the listener by scope.
return;
}
// Finalizable which will remove listener at a safe time. The
// listener may be removed earlier if not referenced in business
// logic by the time this Finalizable executes its finished method,
// and the GC collects it. In the latter case, the listener will not
// be notified, but it doesn't matter, since it is no longer
// referenced by business logic at that point.
final Finalizable cleaner = new Finalizable()
{
public void finished()
{
sessionListeners.remove(listener);
listener.deregisteredSessionListener();
}
public void deleted() {}
public void iterate() {}
public void retry() {}
};
// store the finalizable so we can deregister it explicitly if needed
sessionListeners.put(listener, cleaner);
switch (scope)
{
case GLOBAL:
TransactionManager.registerFinalizable(cleaner, true);
break;
case ENCLOSING_EXTERNAL:
TransactionManager.registerTopLevelFinalizable(cleaner, true);
break;
case CURRENT:
TransactionManager.registerFinalizable(cleaner, false);
break;
case NEXT_EXTERNAL:
TransactionManager.registerNextExternal(cleaner);
break;
default:
break;
}
}
/**
* Explicitly deregister a global session listener, which represents a dynamic temp-table
* resource which has been deleted explicitly.
*
* @param listener
* Listener to deregister.
*/
void deregisterSessionListener(SessionListener listener)
{
sessionListeners.remove(listener);
}
/**
* Getter for the {@code ffCache} field.
*
* @return An instance of the fast find cache, assigned to this context.
*/
FastFindCache getFastFindCache()
{
return ffCache;
}
/**
* Get the record nursery.
*
* @return Record nursery.
*/
RecordNursery getNursery()
{
return nursery;
}
/**
* Retrieve the ORM {@code Session} object associated with this context, if any.
* <p>
* If the current session is broken, return <code>null</code>.
*
* @return The session object associated with the current context, or
* <code>null</code> if none has been created.
*/
Session getSessionNoCreate()
{
return session;
}
/**
* Retrieve the ORM <code>Session</code> object associated with this context, creating it if necessary.
* The session can be closed by {@link #closeSession}.
*
* @return The session object associated with the current context.
*
* @throws PersistenceException
* if an error occurs associating a detached record with the new session.
* @throws IllegalArgumentException
* if <code>database</code> has not been registered with the persistence service.
*/
Session getSession()
throws PersistenceException
{
if (session == null)
{
if (openSessionCount > 0 && LOG.isLoggable(Level.WARNING))
{
LOG.warning("Opening new database session with " + openSessionCount + " already open!");
}
SessionFactory.PersistenceConsumer<Session> initialize = (currentSession) ->
{
session = currentSession;
// associate detached records in the current context with the new session
Iterator<RecordBuffer> iter = bufferManager.activeBuffers(Persistence.this);
RecordBuffer buffer = null;
Record record = null;
boolean finer = LOG.isLoggable(Level.FINER);
while (iter.hasNext())
{
buffer = iter.next();
record = buffer.getCurrentRecord();
// do not associate a template record with the new session
if (record.primaryKey() < 0)
{
continue;
}
if (finer)
{
LOG.log(Level.FINER, message("associating record with session: " +
record.getClass().getName() + "#" +
record.primaryKey()));
}
try
{
session.associate(record);
}
catch (StaleRecordException exc)
{
if (finer)
{
LOG.log(Level.FINE, message("stale record will not be associated with session: " +
record.getClass().getName() + "#" +
record.primaryKey()));
}
}
catch (PersistenceException exc)
{
DBUtils.handleException(tenant.database, exc);
closeSession(true);
String msg = "Error re-associating detached DMO with new session"
+ (" [" + record.getClass().getName() + "#" + record.primaryKey() + "]");
LOG.log(Level.SEVERE, message(msg), exc);
handleException(msg, exc);
}
}
};
session = factory.create(tenant.database, tenant.dmoVersion, initialize, tenant.tenantName);
openSessionCount++;
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, message("opened ORM session; " + openSessionCount + " open"));
/*
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, "Stack trace: ", new Throwable());
}
*/
}
bufferManager.registerPersistenceContext(this);
}
return session;
}
/**
* The name of the tenant that was assigned to this context, if any.
*
* @return The name of the tenant.
*/
String getTenantName()
{
return tenant.tenantName;
}
/**
* The id of the tenant that was assigned to this context.
*
* @return The id of the tenant.
*/
public int getTenantId()
{
return tenant.tenantId;
}
/**
* Indicate whether a database-level transaction currently is open.
*
* @return <code>true</code> if open, else <code>false</code>.
*/
boolean isTransactionOpen()
{
return session != null && session.isOpen() && session.inTransaction();
}
/**
* Get the context-local, dialect-specific data object, if any.
*
* @return Dialect specific data object, or <code>null</code> if no
* such data exists.
*/
Object getDialectData()
{
return dialectData;
}
/**
* Indicate whether auto-commit is needed; this is the case for temp-tables being modified
* outside of an explicit transaction.
*
* @return {@code true} if auto-commit is needed, else {@code false}.
*/
boolean isAutoCommit()
{
return temporary && !isTransactionOpen();
}
/**
* Attempt to close the database session associated with this context, and indicate whether
* the attempt was successful. A close attempt will be unsuccessful if this method is
* called while already in the process of closing the session, or if an attempt is made to
* close a legacy temp-table database session while temporary tables are still in use.
* <p>
* This implementation is tolerant of multiple closes on the same session. If the session
* already was closed or could not be found, the method simply returns {@code false}.
*
* @param error
* {@code true} to indicate the session is closing due to an error; {@code false}
* to indicate the session is closing in the course of normal processing.
*
* @return {@code true} is the session was closed successfully; {@code false} if it was
* not closed because either: (a) this method was invoked in the course of an
* existing attempt to close the session; or (b) the session is with the legacy
* temp-table database while temporary tables are still in use.
*
* @throws PersistenceException
* if an open session was found but the attempt to close it failed with a database
* error.
*/
boolean closeSession(boolean error)
throws PersistenceException
{
return closeSessionImpl(error, error);
}
/**
* Attempt to close the database session associated with this context, and indicate whether
* the attempt was successful. A close attempt will be unsuccessful if this method is
* called while already in the process of closing the session, or if an attempt is made to
* close a legacy temp-table database session while temporary tables are still in use.
* <p>
* This implementation is tolerant of multiple closes on the same session. If the session
* already was closed or could not be found, the method simply returns {@code false}.
*
* @param error
* {@code true} to indicate the session is closing due to an error; {@code false}
* to indicate the session is closing in the course of normal processing.
* @param force
* {@code true} to force the session to be closed and all database resources
* associated with it to be cleaned up.
*
* @return {@code true} is the session was closed successfully; {@code false} if it was
* not closed because either: (a) this method was invoked in the course of an
* existing attempt to close the session; or (b) the session is with the legacy
* temp-table database while temporary tables are still in use.
*
* @throws PersistenceException
* if an open session was found but the attempt to close it failed with a database
* error.
*/
private boolean closeSessionImpl(boolean error, boolean force)
throws PersistenceException
{
// prevent nested or invalid close attempts
if (closingSession || session == null || !session.isOpen())
{
return false;
}
try
{
closingSession = true;
if (isTransactionOpen())
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, message("rolling back orphaned transaction"));
}
rollback(error);
}
if (!error && !force && temporary && TemporaryBuffer.getTableCount() > 0)
{
// allow unused tables to be cleaned up
if (!sessionListeners.isEmpty())
{
notifySessionEvent(SessionListener.Event.SESSION_CLEANUP);
}
// Special treatment for temp table sessions. These must
// remain open as long as any table exists, otherwise the
// table and its data will be lost.
return false;
}
try
{
try
{
if (!sessionListeners.isEmpty())
{
SessionListener.Event event = error
? SessionListener.Event.SESSION_CLOSING_WITH_ERROR
: SessionListener.Event.SESSION_CLOSING_NORMALLY;
notifySessionEvent(event);
}
}
finally
{
if (factory.isEnabled())
{
factory.markReclaimable(session);
}
else
{
session.close();
}
}
}
catch (PersistenceException exc)
{
DBUtils.handleException(tenant.database, exc);
handleException("error closing database session", exc);
}
return true;
}
finally
{
session = null;
openSessionCount--;
// DEBUG LOG.finer(database.toString() + "...closed database session; sessionUseCount = " + sessionUseCount);
sessionUseCount = 0;
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, message("closed database session; " + openSessionCount + " open"));
/*
if (LOG.isLoggable(Level.FINEST))
{
LOG.log(Level.FINEST, "Stack trace: ", new Throwable());
}
*/
}
closingSession = false;
}
}
/**
* Notify all registered session listeners that the current ORM session is about to
* close or the current, implicit transaction is being committed. Listeners which were set
* to be deregistered upon an event are deregistered after they are notified. There is no
* guaranteed order in which listeners are notified.
* <p>
* All registered listeners are given a chance to process this event, even if one or more
* raise a <code>PersistenceException</code> (there is no such guarantee for unchecked
* exceptions). Each exception caught is logged.
*
* @param event
* Session event type.
*/
private void notifySessionEvent(SessionListener.Event event)
{
Iterator<SessionListener> iter = sessionListeners.keySet().iterator();
while (iter.hasNext())
{
SessionListener listener = iter.next();
boolean deregister = false;
try
{
// allow listener to process session event
deregister = listener.sessionEvent(event);
}
catch (PersistenceException exc)
{
// deregister listener if there was an error
deregister = true;
LOG.log(Level.SEVERE, "Error processing session event", exc);
}
finally
{
// remove listeners if there was an error, or if the listener indicated it should
// be removed in response to the session closing notification
if (deregister)
{
iter.remove();
listener.deregisteredSessionListener();
}
}
}
}
/**
* Cleanup hook invoked at the end of the current client context. Close the currently open
* database session, if any.
*/
void cleanup()
{
if (isTransactionOpen())
{
// Under normal circumstances, there should not be any tx open
// at the context's end of life, but just in case...
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
message("rolling back open transaction during context cleanup"));
}
try
{
rollback(false);
}
catch (PersistenceException exc)
{
LOG.log(Level.SEVERE, "Error rolling back open transaction during context cleanup", exc);
}
}
try
{
closeSessionImpl(false, true);
}
catch (PersistenceException exc)
{
LOG.log(Level.SEVERE, "Error closing database session during context cleanup", exc);
}
finally
{
// Deregister the SessionFactory of the Context from the SessionCloseThread
// If the factory has an open reclaimable session, it closes it.
SessionFactory.SessionCloseThread.deregister(factory);
Database database = tenant.database;
if (database.isTemporary())
{
PersistenceFactory.remove(database);
}
}
}
/**
* Checks if the rereadnolock 4GL command-line option is active for this client context.
*
* @return {@code true} if rereadnolock is specified for this client context
*/
boolean isRereadNoLock()
{
return rereadNoLock;
}
/**
* Decide whether this persistence context is unsafe or not.
*
* @param value
* If true the persistence will run in unsafe mode and in safe mode if false.
*/
void setUnsafe(boolean value)
{
unsafe = value;
}
/**
* Checks whether this persistence context is unsafe or not.
*
* @return If true the persistence will run in unsafe mode and in safe mode if false.
*/
boolean isUnsafe()
{
return unsafe;
}
/**
* Return a short description of this object. To be as concise as possible, the returned string contains
* only the physical database name.
*
* @return a short description of this object.
*/
@Override
public String toString()
{
return "Persistence$Context{" + tenant.database + "}";
}
}
}