Class SimpleJsonDataOperator<T extends BaseDataEntity<String>>
- Type Parameters:
T- Data type inherited from BaseDataEntity
- All Implemented Interfaces:
Cached,DataOperator<T>,RowCountingDelete
- Version:
- 1.0.0
- Author:
- wisdomme
-
Nested Class Summary
Nested classes/interfaces inherited from interface com.ultikits.ultitools.interfaces.DataOperator
DataOperator.LikeType -
Field Summary
FieldsModifier and TypeFieldDescriptionSQL column name (lowercase) -> Java field name.Keys whose committed value (or absence) has changed since the last checkpoint that captured them, each with the generation of its latest change.private static final com.google.gson.GsonDefault Gson has no bundled adapter forjava.time.LocalDateTime: its reflective fallback tries to reachLocalDateTime's private fields, which JDK 9+'s module system refuses without--add-opens java.base/java.time, throwingJsonIOExceptionthe first time this operator serializes an entity carrying a non-nullLocalDateTime-typed@Column(02-08 --AuditableDataEntity#createdAt/updatedAtwere alwaysnullbefore this plan, so the whole-entityGSON.toJson(...)calls below never reached this type until the lifecycle hooks started populating it).private final StringThis table's name in journal records: its directory's path relative to the identity root.private final JsonLockTableThe table write locks this operator's writes take:JsonStore's shared lock table, or, for an operator constructed directly, a lock table of its own.private static final Loggerprivate final Stringprivate final JsonTransactionManagerTheJsonTransactionManagerevery read and write of this operator goes through: the identity's manager, whichJsonStorepasses in, or, for an operator constructed directly, the manager of a standalone identity of its own (seeSimpleJsonDataOperator(String, Class)). -
Constructor Summary
ConstructorsConstructorDescriptionSimpleJsonDataOperator(String storeLocation, Class<T> type) An operator over the table directorystoreLocation, with an identity of its own: its transactions journal in<storeLocation>.ultitools-journal, a sibling of the table directory, and it has its own table locks.SimpleJsonDataOperator(String storeLocation, Class<T> type, JsonTransactionManager transactionManager, JsonLockTable lockTable) An operator of an identityJsonStoremanages: it uses that identity's manager (and so its journal, commit gate and visibility lock) and the store's lock table, and registers with the identity so the identity's checkpoints capture its changes. -
Method Summary
Modifier and TypeMethodDescription(package private) voidapplyStaged(JsonTransaction.Overlay<T> overlay) Applies one transaction's staged writes for this operator to the committed entries and marks each key dirty.buildColumnToFieldName(Class<?> type) Builds the "SQL column name -> Java field name" mapping from the entity class's@Columnannotations.(package private) voidcaptureDirty(List<JsonIdentity.Captured> captured) Adds every dirty key of this operator, with its generation and its committed value (ornullif the entry is gone), tocaptured.(package private) JsonJournal.EntrycheckpointEntry(Object key, Object value) The journal entry that bringskey's file tovalue: the entity's JSON (the form this operator has always written to its files) for a value, a delete fornull.(package private) voidclearDirty(Object key, long generation) Clearskey's dirty mark if it still carriesgeneration: a key written again after a checkpoint's capture keeps its newer mark for the next checkpoint.private booleanconditionCal(String data, String value, WhereCondition condition) voiddel(WhereCondition... whereConditions) Deletes by predicate, without loading matched entries into entities first.voidDeletes by id, fetching the entity first soonDelete()can fire on it -- unlikedel(WhereCondition...), which deletes by predicate without materializing entries and therefore does not fire the hook.intdelById(Object), returning how many cache entries it removed (#521), soQuery#delete()can return rows removed instead of rows matched.private voiddelLocked(JsonTransaction transaction, WhereCondition... whereConditions) del(WhereCondition...)'s body after the refusal, run holding this table's write lock.private TReturns a detached copy ofentity, produced by the same Gson form this operator persists to disk (#522).private List<WhereCondition> evaluableConditions(WhereCondition... expected) The non-empty conditions of anupdateIf(T, com.ultikits.ultitools.entities.WhereCondition...)call, each checked before anything is read: anullcondition, a column the entity does not map with@Column, anullvalue under any comparison other thanComparison.EQUAL(under EQUAL it means "absent or JSON null", #640), and a non-serialisable value are refused with aDataAccessException, as the relational operators refuse them.booleanexist(WhereCondition... whereConditions) Check if the data record exists.booleanLooks the entry up by the entity's id, as the relational operators do (#522).voidflush()Persist cache object.voidgc()Compare local with cache and delete local persistent files.getAll()Get all data record.getAll(WhereCondition... whereConditions) FiresonLoad()once per entity actually returned to the caller.getAllRaw(WhereCondition... whereConditions) Get data record by ID.getLike(String column, String value, DataOperator.LikeType likeType) Fuzzy QueryvoidInsert data record.voidInsert multiple entities atomically: on every shipped backend either all of them are stored or, if one fails, none is.private voidinsertLocked(JsonTransaction transaction, T obj) insert(T)'s body, run holding this table's write lock.(package private) voidjournalEntries(JsonTransaction.Overlay<T> overlay, List<JsonJournal.Entry> record) Adds the journal entries of one transaction's staged writes for this operator torecord, in staging order: the entity's JSON for a put (the form a checkpoint writes to its file), a delete for a staged delete.(package private) JsonLockTableThe lock table this operator's writes lock in.private voidMarkskeychanged since the last checkpoint, with a fresh generation.private booleanWhether one serialised entry satisfies one condition -- the single matching rule shared bygetAll(WhereCondition...),del(WhereCondition...)andupdateIf(T, com.ultikits.ultitools.entities.WhereCondition...).mergedEntries(JsonTransaction transaction) Every entrytransactionsees (every committed entry when it isnull): the committed entries whose key has no staged write, then the staged puts.private TmergedFind(JsonTransaction transaction, Object id) mergedGet(com.ultikits.ultitools.interfaces.impl.data.json.JsonTransaction, java.lang.Object)by an entity id, trying its string form when the key type differs.private TmergedGet(JsonTransaction transaction, Object key) The entry forkeyastransactionsees it: its staged write when it has one (nullfor a staged delete), otherwise the committed entry.page(int page, int size, WhereCondition... whereConditions) Get data record by page.private <R> RpublishPlainWrite(Supplier<R> apply) Applies a write made outside any transaction: with the identity's commit gate held (read side), then the visibility write side, so a checkpoint's capture sees it whole or not at all and every reader sees it at one instant.private <R> RreadVisible(Function<JsonTransaction, R> read) Runsreadwith the calling thread's transaction (ornulloutside one) under the visibility read side.private voidreplaceEntry(T stored, T source) Replacesstoredwith a copy carrying every field ofsourceexceptid, in one step -- staged in the calling thread's transaction, or put into the committed entries under the visibility write side -- so a concurrent read never sees a half-copied entry.private voidRefuses a new id whose file<id>.jsonthe insert must not create (question 12 A of the Phase 20.4 decisions): a Windows device name with or without an extension, a trailing dot or space,.or.., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to<table directory>/<id>.json(on Windows,< > : " | ? *, including a leading drive prefix such asc:).private voidRefuses an update or delete addressed by anullid (#546), which no entry can have as its key; the cache is aConcurrentHashMap, so it used to surface as a rawNullPointerException.private voidrequireStorableKey(Object key) Refuses an id that cannot name this table's entity file<id>.json: an empty id, or one containing a slash, a backslash or NUL.private StringresolveColumn(String column) Resolves the caller-supplied column name into the key used in the Gson-serialized map.private voidStoresdetachedValueunderkey: staged in the calling thread's transaction, or, outside one, put into the committed entries in one step (seepublishPlainWrite(Supplier)).private StringstripJsonQuotes(String jsonValue) Remove JSON quotes from a serialized string value.(package private) StringThe name of this operator's table for messages: its@Tablevalue, else the entity's simple name.voidtransaction(Runnable action) Execute operations within a transaction (void variant), with the same guarantees asDataOperator.transaction(Callable)on every shipped backend, including the JSON backend's table lock at the first read of a transaction this method begins.<R> Rtransaction(Callable<R> action) Execute operations within a transaction.(package private) JsonTransactionManagerThe manager this operator uses, fixed at construction.voidUpdate one field of one record.voidUpdate data record by object.voidUpdate multiple entities atomically: on every shipped backend either all of them are updated or, if one fails, none is.intupdateCounted(T entity) Counted update:DataOperator.update(BaseDataEntity), returning how many rows it wrote, so the caller can tell that nothing was written (#558).private intupdateEntry(T obj) update(BaseDataEntity), returning how many entries it changed: an id no entry has writes nothing and logs one WARNING, where it used to throw a rawNullPointerException(#558).booleanupdateIf(T entity, WhereCondition... expected) Conditional update: writesentityover the stored row with the same id only if that row still matches every one ofexpected, and reports whether it did (#543).private booleanupdateIfLocked(T entity, Object id, List<WhereCondition> conditions) updateIf(T, com.ultikits.ultitools.entities.WhereCondition...)'s check and write, run holding this table's write lock.The values the calling thread sees, collected under the visibility read side.private voidwarnNoEntry(Object id) Logs one WARNING for an update by a non-null id that matches no entry (#558, maintainer decision 2026-09-29), naming the store by its@Tablevalue -- the directory it is kept in -- as the relational backends name the table.private <R> Rwrite(Function<JsonTransaction, R> body) Runs one write while holding this table's write lock.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface com.ultikits.ultitools.interfaces.DataOperator
query
-
Field Details
-
LOGGER
-
GSON
private static final com.google.gson.Gson GSONDefault Gson has no bundled adapter forjava.time.LocalDateTime: its reflective fallback tries to reachLocalDateTime's private fields, which JDK 9+'s module system refuses without--add-opens java.base/java.time, throwingJsonIOExceptionthe first time this operator serializes an entity carrying a non-nullLocalDateTime-typed@Column(02-08 --AuditableDataEntity#createdAt/updatedAtwere alwaysnullbefore this plan, so the whole-entityGSON.toJson(...)calls below never reached this type until the lifecycle hooks started populating it). MirrorsAbstractRelationalDataOperator's identical adapter (02-08), serializing to/fromLocalDateTime.toString()'s ISO-8601 form. -
storeLocation
-
type
-
cache
-
columnToFieldName
SQL column name (lowercase) -> Java field name.Callers always query using the SQL column name declared on
@Column(52 call sites across 17 Modules), but the key Gson serializes into the map is the Java field name -- the two don't line up on snake_case vs. camelCase, and the query would silently return zero hits. This map translates the former into the latter, matching the read pathAbstractRelationalDataOperator#getListHandler()already uses. See issue #176. -
transactionManager
TheJsonTransactionManagerevery read and write of this operator goes through: the identity's manager, whichJsonStorepasses in, or, for an operator constructed directly, the manager of a standalone identity of its own (seeSimpleJsonDataOperator(String, Class)). Fixed at construction, so this operator's readers and committers always use the same transaction contexts, the same visibility lock and the same journal. -
lockTable
The table write locks this operator's writes take:JsonStore's shared lock table, or, for an operator constructed directly, a lock table of its own. -
journalTable
This table's name in journal records: its directory's path relative to the identity root. -
dirty
Keys whose committed value (or absence) has changed since the last checkpoint that captured them, each with the generation of its latest change. Written under the commit gate's read side and the visibility write side; a checkpoint captures it under the gate's write side.
-
-
Constructor Details
-
SimpleJsonDataOperator
An operator over the table directorystoreLocation, with an identity of its own: its transactions journal in<storeLocation>.ultitools-journal, a sibling of the table directory, and it has its own table locks. Before it reads the table directory it recovers that journal, so the entries it loads include every transaction committed before a crash. Servers reach JSON data throughJsonStore, whose operators share their plugin's identity; this constructor serves code that builds an operator directly.- Parameters:
storeLocation- the table directory, one<key>.jsonfile per entitytype- the entity type- Throws:
DataAccessException- naming the file if a committed journal record is damaged; nothing is changed, and constructing the operator again succeeds once the file has been moved out of the journal directory
-
SimpleJsonDataOperator
SimpleJsonDataOperator(String storeLocation, Class<T> type, JsonTransactionManager transactionManager, JsonLockTable lockTable) An operator of an identityJsonStoremanages: it uses that identity's manager (and so its journal, commit gate and visibility lock) and the store's lock table, and registers with the identity so the identity's checkpoints capture its changes. Recovers the identity's journal before reading the table directory (forJsonStoreit already has, and this is a no-op), so the table files read here include every committed record.- Parameters:
storeLocation- the table directory, a direct child of the identity roottype- the entity typetransactionManager- the identity's managerlockTable- the store's lock table
-
-
Method Details
-
transactionManager
JsonTransactionManager transactionManager()The manager this operator uses, fixed at construction.- Returns:
- this operator's transaction manager, never
null
-
lockTable
JsonLockTable lockTable()The lock table this operator's writes lock in. -
tableName
String tableName()The name of this operator's table for messages: its@Tablevalue, else the entity's simple name. -
write
Runs one write while holding this table's write lock. Inside a transaction the transaction first rejects the write if it is read-only or past its deadline, then takes the lock, beforebodyevaluates or stages anything, and holds it until the transaction ends; outside one the lock is held for the duration of the call. Every visibility-lock stepbodytakes comes after the table lock, so no thread waits for a table lock while holding the visibility lock.- Type Parameters:
R- the write's result- Parameters:
body- the write, given the calling thread's transaction ornulloutside one- Returns:
- what
bodyreturned - Throws:
DataAccessException- if the transaction is read-only or past its deadline, or the lock wait fails (deadlock or lock-wait timeout)
-
readVisible
Runsreadwith the calling thread's transaction (ornulloutside one) under the visibility read side.readonly collects references; conditions, detaching and hooks run after this returns. Inside a transaction the transaction first checks its deadline and, atREPEATABLE_READorSERIALIZABLE, takes this table's lock -- before the read side is taken, never while holding it. -
mergedGet
The entry forkeyastransactionsees it: its staged write when it has one (nullfor a staged delete), otherwise the committed entry. Outside a transaction, the committed entry. Called under the visibility read side. -
mergedFind
mergedGet(com.ultikits.ultitools.interfaces.impl.data.json.JsonTransaction, java.lang.Object)by an entity id, trying its string form when the key type differs. -
mergedEntries
Every entrytransactionsees (every committed entry when it isnull): the committed entries whose key has no staged write, then the staged puts. Called under the visibility read side; collects references only. Committed values are never changed in place, so the references stay valid after the lock is released. -
visibleValues
The values the calling thread sees, collected under the visibility read side. -
store
StoresdetachedValueunderkey: staged in the calling thread's transaction, or, outside one, put into the committed entries in one step (seepublishPlainWrite(Supplier)). -
publishPlainWrite
Applies a write made outside any transaction: with the identity's commit gate held (read side), then the visibility write side, so a checkpoint's capture sees it whole or not at all and every reader sees it at one instant. The caller holds this table's write lock. Plain writes are not journaled: they reach disk at the next checkpoint, as before 6.3.0.applyupdates in-memory maps only and marks every key it changes dirty. -
markDirty
Markskeychanged since the last checkpoint, with a fresh generation. -
applyStaged
Applies one transaction's staged writes for this operator to the committed entries and marks each key dirty. Called only byJsonTransaction.commit(), after the transaction's journal record is committed, holding this table's write lock, the commit gate's read side and the visibility write side. Takes no lock of its own, so a commit holding the write side never waits for anything.- Parameters:
overlay- the staged writes; anullvalue is a staged delete
-
journalEntries
Adds the journal entries of one transaction's staged writes for this operator torecord, in staging order: the entity's JSON for a put (the form a checkpoint writes to its file), a delete for a staged delete.- Throws:
IllegalArgumentException- if a key is not one path segment
-
checkpointEntry
The journal entry that bringskey's file tovalue: the entity's JSON (the form this operator has always written to its files) for a value, a delete fornull.- Throws:
IllegalArgumentException- ifkeyis not one path segment
-
captureDirty
Adds every dirty key of this operator, with its generation and its committed value (ornullif the entry is gone), tocaptured. Called byJsonIdentity.checkpoint()under the commit gate's write side, when no commit or plain write is applying. Collects references only; committed values are never changed in place. -
clearDirty
Clearskey's dirty mark if it still carriesgeneration: a key written again after a checkpoint's capture keeps its newer mark for the next checkpoint. -
requireStorableKey
Refuses an id that cannot name this table's entity file<id>.json: an empty id, or one containing a slash, a backslash or NUL. Such an entry could never be journaled or written by a checkpoint; before 6.3.0 it was flushed into a sub-path and read back as a corrupt entry. -
requireCreatableKey
Refuses a new id whose file<id>.jsonthe insert must not create (question 12 A of the Phase 20.4 decisions): a Windows device name with or without an extension, a trailing dot or space,.or.., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to<table directory>/<id>.json(on Windows,< > : " | ? *, including a leading drive prefix such asc:). SeeJsonFileNames. Called only for a key the insert creates, so an existing file whose name these rules would refuse keeps working. -
buildColumnToFieldName
Builds the "SQL column name -> Java field name" mapping from the entity class's@Columnannotations. Collected along the inheritance chain, so the@Column("id")inherited fromBaseDataEntityis included too.- Parameters:
type- the entity type- Returns:
- an unmodifiable map keyed by the lowercased SQL column name
-
resolveColumn
Resolves the caller-supplied column name into the key used in the Gson-serialized map.- Parameters:
column- the caller-supplied column name, which may be a SQL column name, a Java field name, or a nested path- Returns:
- a key or path that can be passed directly to
JsonPathUtil
-
detach
Returns a detached copy ofentity, produced by the same Gson form this operator persists to disk (#522).Every read path hands out one of these rather than the instance held in
cache, andinsert(T)stores one rather than the caller's instance. Before 6.3.0 the cached instances themselves were returned, so changing a loaded entity without callingupdate(...)changed the store and was written at the next flush -- ondatasource.type: jsononly; the relational backends materialise every row afresh, so there a change withoutupdate(...)never persisted. The copy is what makes the same module code behave the same on every backend.- Parameters:
entity- the cached (or caller-supplied) instance- Returns:
- a new instance equal in every persisted field
-
requireId
Refuses an update or delete addressed by anullid (#546), which no entry can have as its key; the cache is aConcurrentHashMap, so it used to surface as a rawNullPointerException. MirrorsAbstractRelationalDataOperator's refusal. -
detachAll
-
exist
Looks the entry up by the entity's id, as the relational operators do (#522). Comparing the caller's instance with the cached one throughequals()stopped matching once reads and inserts were detached: an entity whoseequals()covers a field it changed without callingupdate(...)no longer equals the stored copy.- Specified by:
existin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
object- Data record entity- Returns:
- Whether the record exists
-
exist
Description copied from interface:DataOperatorCheck if the data record exists.- Specified by:
existin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
whereConditions- Conditions- Returns:
- Whether the record exists
-
getById
Description copied from interface:DataOperatorGet data record by ID.- Specified by:
getByIdin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
id- Record ID- Returns:
- Data record
-
getAll
Description copied from interface:DataOperatorGet all data record.- Specified by:
getAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Returns:
- Data record list
-
getAll
FiresonLoad()once per entity actually returned to the caller. Delegates the matching logic togetAllRaw(com.ultikits.ultitools.entities.WhereCondition...), whichexist(WhereCondition...)andpage(int, int, com.ultikits.ultitools.entities.WhereCondition...)also call directly --existbecause a boolean check should not fire a load hook, andpagebecause firing on every matched row (rather than only the page's slice) would fireonLoad()for entities the caller never actually receives.- Specified by:
getAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
whereConditions- Conditions- Returns:
- Data record list
-
getAllRaw
-
getLike
Description copied from interface:DataOperatorFuzzy Query- Specified by:
getLikein interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
column- Column namevalue- Query valuelikeType- Like type- Returns:
- Data record list
-
page
Description copied from interface:DataOperatorGet data record by page.- Specified by:
pagein interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
page- Page numbersize- Page sizewhereConditions- Conditions- Returns:
- Data record list
-
insert
Insert data record.The entity is stored as
<id>.json. An insert of an id no entry has yet refuses, with aDataAccessException(DATA_ENTITY_INVALID) naming the id, an id that cannot name a portable file: a Windows device name with or without an extension, a trailing dot or space,.or.., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to<table directory>/<id>.json(on Windows,< > : " | ? *, including a leading drive prefix such asc:); seeJsonFileNames. An insert of an id that already has an entry changes nothing and is never refused, so a file whose name these rules would refuse keeps working. A refusal does not mark the transaction rollback-only.- Specified by:
insertin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
obj- Data record
-
insertLocked
insert(T)'s body, run holding this table's write lock. -
del
Deletes by predicate, without loading matched entries into entities first. Consequently this overload does not fireonDelete()-- there is no entity to fire it on without fabricating one, and this class does not fabricate entities to satisfy a hook.delById(Object)fetches the entity first and does fireonDelete()on it (02-08-PLAN.md's delete-hook-path split, mirrored fromAbstractRelationalDataOperator).A
nullor zero-lengthwhereConditionsis refused with aDataAccessException, matchingAbstractRelationalDataOperator.del()'s guard and the contractDataOperator.del(com.ultikits.ultitools.entities.WhereCondition...)'s interface javadoc promises (CR-02, 02-13). The refusal runs before anything is read or staged, so a refused call inside a transaction stages nothing.Inside a transaction, every entry the transaction sees that matches is staged as a delete; outside one, the matches are removed in one step under the visibility write side. Either way the matches are found while holding this table's write lock.
- Specified by:
delin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
whereConditions- Conditions
-
delLocked
del(WhereCondition...)'s body after the refusal, run holding this table's write lock. -
delById
Deletes by id, fetching the entity first soonDelete()can fire on it -- unlikedel(WhereCondition...), which deletes by predicate without materializing entries and therefore does not fire the hook. If no entry matchesid, nothing fires. Reads the stored entry directly (notgetById(java.lang.Object)) so this does not also fireonLoad()-- deleting is not loading.onDelete()fires on a detached copy of the entry being deleted (inside a transaction, of the entry as that transaction sees it), so the hook sees every persisted field and nothing it changes on the entity reaches a committed value -- not even through a delete that is then rolled back.- Specified by:
delByIdin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
id- Record ID
-
deleteByIdCounted
delById(Object), returning how many cache entries it removed (#521), soQuery#delete()can return rows removed instead of rows matched.- Specified by:
deleteByIdCountedin interfaceRowCountingDelete- Parameters:
id- the entry id- Returns:
1if an entry with this id was removed,0if there was none
-
update
Description copied from interface:DataOperatorUpdate one field of one record.- Specified by:
updatein interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
column- Column namevalue- New valueid- Record ID
-
update
Description copied from interface:DataOperatorUpdate data record by object. It will not update the fields that the incoming entity does not have.- Specified by:
updatein interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
obj- Data record
-
updateCounted
Counted update:DataOperator.update(BaseDataEntity), returning how many rows it wrote, so the caller can tell that nothing was written (#558).An update by a non-null id that matches no row writes nothing on every backend: SQLite, MySQL and JSON each log one WARNING naming the table and the id, and return normally.
update(T)gives the caller no way to see that; this method returns0instead of1. Use it wherever "the row was gone" must be treated as a failure -- for example a write that credits an account that another server has just removed. For a write that should apply only while the stored row still has the values you read, useDataOperator.updateIf(T, com.ultikits.ultitools.entities.WhereCondition...)instead.The framework's operators return the backend's own affected-row count. This default, which a third-party implementation inherits unless it overrides it, checks with
DataOperator.exist(WhereCondition...)whether a row with the entity's id exists, returns0without writing if it does not, and otherwise callsupdate(T)and returns1. It cannot see what thatupdatewrote, so one case remains in which it reports1although nothing was written: another writer deleting the row during that one call.Returns the number of entries replaced, decided while holding this table's write lock.
- Specified by:
updateCountedin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entity- the new state of the row, carrying the id of the row to write- Returns:
- the number of rows written:
1, or0when no row has that id
-
updateEntry
update(BaseDataEntity), returning how many entries it changed: an id no entry has writes nothing and logs one WARNING, where it used to throw a rawNullPointerException(#558). -
warnNoEntry
Logs one WARNING for an update by a non-null id that matches no entry (#558, maintainer decision 2026-09-29), naming the store by its@Tablevalue -- the directory it is kept in -- as the relational backends name the table. -
replaceEntry
Replacesstoredwith a copy carrying every field ofsourceexceptid, in one step -- staged in the calling thread's transaction, or put into the committed entries under the visibility write side -- so a concurrent read never sees a half-copied entry. Shared byupdate(BaseDataEntity)andupdateIf(T, com.ultikits.ultitools.entities.WhereCondition...).BeanCopyUtilcopies field references, so the merged copy still shares the caller's List, Map and other mutable field values; a detached copy of it is what gets stored, so no object reachable from the caller's entity is reachable from a committed or staged value (C11). A change the caller makes to its entity afterwards changes nothing stored. -
updateIf
Conditional update: writesentityover the stored row with the same id only if that row still matches every one ofexpected, and reports whether it did (#543).The check and the write are one step: on SQLite and MySQL a single
UPDATE ... WHERE id = ? AND <expected>whose affected-row count decides the result, so it holds across servers sharing one database; on the JSON backend the check and the write run under the operator's own lock (a JSON store is local to one server). The conditions mean exactly what they mean inDataOperator.getAll(WhereCondition...)on the same backend, values are bound as parameters, and a condition whoseisEmpty()is true is ignored.Typical use is compare-and-set: read a row, compute the new state, and write it conditioned on the value that was read; if another writer changed the row in between, nothing is written and
falsecomes back, so the caller re-reads and decides again rather than writing on top. UltiEconomy's one-time wallet merge uses it this way, conditioned on the balance it read:
LikeAccount read = accounts.getById(id); double seen = read.getBalance(); read.setBalance(seen + amount); if (!accounts.updateIf(read, WhereCondition.builder().column("balance").value(seen).build())) { // someone else wrote first: re-read and decide again }DataOperator.update(BaseDataEntity), every mapped field ofentityis written, andonUpdate()fires onentitybefore the fields are read, whether or not the write then applies.A
nullexpected value meansIS NULLunder the defaultComparison.EQUALcomparison: the condition holds only while the stored column is unset -- on SQLite and MySQL<column> IS NULLinside the same single statement, on JSON an entry whose field is absent or JSON null. A compare-and-set against a column that was unset when it was read therefore misses, and keeps the value, when another writer filled the column in between. This meaning belongs toupdateIfalone:DataOperator.getAll(WhereCondition...),DataOperator.exist(WhereCondition...),DataOperator.del(WhereCondition...)andDataOperator.page(int, int, WhereCondition...)treat anullcondition value as they always have. (6.3.0 snapshots published before this change refused anullexpected value; no release did.)The entry the caller sees -- inside a transaction, the transaction's staged version of it -- is checked against
expectedwith the same matchinggetAll(WhereCondition...)uses, and replaced, while this table's write lock is held, so no other write -- a plain write or another transaction's -- can come between the check and the write. Inside a transaction the lock is held until the transaction ends, so a second transaction'supdateIfon the same table is checked only after the first has committed or rolled back: of two calls expecting the same value, at most one succeeds. A JSON store belongs to one server; nothing here coordinates two servers.A
nullexpected value under the defaultComparison.EQUALis satisfied by an entry whose field is absent or JSON null, and by no present value (#640). This meaning belongs to this method alone:getAll(WhereCondition...)anddel(WhereCondition...)are unchanged.- Specified by:
updateIfin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entity- the new state of the row, carrying the id of the row to writeexpected- the conditions the stored row must still meet- Returns:
trueif the row matched and was written;falseif no row with that id matched every condition, in which case nothing was written
-
updateIfLocked
updateIf(T, com.ultikits.ultitools.entities.WhereCondition...)'s check and write, run holding this table's write lock. -
evaluableConditions
The non-empty conditions of anupdateIf(T, com.ultikits.ultitools.entities.WhereCondition...)call, each checked before anything is read: anullcondition, a column the entity does not map with@Column, anullvalue under any comparison other thanComparison.EQUAL(under EQUAL it means "absent or JSON null", #640), and a non-serialisable value are refused with aDataAccessException, as the relational operators refuse them.getAll(WhereCondition...)would simply match nothing for an unknown column, but a conditional write that can never apply would make the caller's re-read-and-retry loop spin forever. -
matches
Whether one serialised entry satisfies one condition -- the single matching rule shared bygetAll(WhereCondition...),del(WhereCondition...)andupdateIf(T, com.ultikits.ultitools.entities.WhereCondition...). An entry that lacks the condition's column never matches.valueJsonis the condition's value already serialised, once per condition rather than once per entry; each caller has already checked that the value is serialisable. -
flush
public void flush()Persist cache object.Runs a checkpoint of this operator's identity (
JsonIdentity.checkpoint()): every entry changed since the last checkpoint, of every table of the plugin, is written through one checkpoint record, so a crash during the flush leaves either the state before it or the state it captured, never a half-written table and never an older value. Entries that did not change are not rewritten. Waits for a checkpoint of the same plugin already running, and for a commit that is between its record and its apply; takes no table lock. Never writes staged data of an open transaction.- Specified by:
flushin interfaceCached- Throws:
DataAccessException- (DATA_PERSISTENCE_FAILED) if the data could not be written; the changed entries stay pending for the next flush
-
gc
public void gc()Compare local with cache and delete local persistent files.Deletes every
*.jsonfile of this table that has no committed entry, except the file of a key with a pending dirty mark: a deletion that has not been checkpointed yet reaches disk only through the next checkpoint's record, which is what keeps plain writes crash atomic per flush. Holds the identity's checkpoint mutex, so no checkpoint writes files meanwhile. -
transaction
Execute operations within a transaction. All operations commit together or roll back on exception.Every backend this framework ships (SQLite, MySQL, JSON) runs the action in a transaction: it joins the transaction already active on the calling thread, if any, and any
Throwablethe action throws rolls it back and is rethrown. SQLite and MySQL use a database transaction. On JSON, the action's writes are staged until commit and are invisible to other threads until then; the commit applies them together, so other threads see all of them at once; a rollback discards them.On JSON, a transaction this method begins locks each table at its first read or write and holds the lock until it commits or rolls back, so a read-modify-write inside the action cannot lose an update, as in 6.2.5, where each JSON
transaction(...)callback ran alone per operator. A read inside the action can therefore wait for another transaction that holds the table: at most 5 seconds, then it fails with aDataAccessExceptionand the transaction rolls back. A call that joins an active transaction keeps that transaction's isolation: inside a@Transactionalmethod at the default isolation, JSON reads take no table lock.When the commit itself fails: on SQLite and MySQL, the exception does not tell whether the database stored the transaction (the outcome is unknown; see
TransactionManager.commit()), and nothing is retried, so code that must not apply a write twice re-reads the data, or usesDataOperator.updateIf(T, com.ultikits.ultitools.entities.WhereCondition...), before trying again. On JSON a failed commit commits nothing. AnUnexpectedRollbackExceptionfrom the commit means the transaction was rolled back, none of its writes stored, on every backend.This default implementation, used only by a third-party operator that does not override it, runs the action directly without a transaction.
Runs
actionin a transaction of this operator'sJsonTransactionManager: begins one, or joins the one already active on this thread, runs the action, and commits. Writes the action makes through any operator of the same plugin are staged until the outermost commit, invisible to other threads until then, and applied together at that commit, so other threads see all of them at once. AnyThrowablethe action throws -- anErrorincluded (#634) -- rolls the transaction back, which discards the staged writes and never touches committed data, and is rethrown as the same instance; a rollback failure is attached to it as suppressed. A commit that throws (a record that cannot be written, orUnexpectedRollbackExceptionfor a transaction marked rollback-only) has already ended the transaction and is not followed by a rollback. MirrorsAbstractRelationalDataOperator#transaction(Callable).Isolation (question 11 A of the Phase 20.4 decisions). A transaction this method begins locks each table at its first read or write, the
REPEATABLE_READbehaviour ofJsonTransactionManager: once the action has read a table, no other transaction or plain write can change it until this transaction ends, so a read-modify-write inside the action cannot lose an update. This keeps the guarantee 6.2.5 gave by running eachtransaction(...)callback alone per operator. The cost is the same as there: a read inside the action can wait for another transaction that holds the table, at most 5 seconds, after which it fails with aDataAccessExceptionand the transaction rolls back; a wait that would close a cycle of transactions waiting for each other fails at once. The table lock is requested before the read takes the visibility lock, never while holding it. A call that joins a transaction already active on this thread (a@Transactionalmethod, or an outertransaction(...)) keeps that transaction's isolation, as a participating scope does in Spring and as a JDBC transaction keeps the isolation it began with: inside a default@Transactionalmethod, reads stay at read committed and take no table lock.- Specified by:
transactionin interfaceDataOperator<T extends BaseDataEntity<String>>- Type Parameters:
R- the return type- Parameters:
action- the operations to execute- Returns:
- the result of the action
- Throws:
Exception- if the action fails
-
transaction
Description copied from interface:DataOperatorExecute operations within a transaction (void variant), with the same guarantees asDataOperator.transaction(Callable)on every shipped backend, including the JSON backend's table lock at the first read of a transaction this method begins. The same commit outcomes apply: seeDataOperator.transaction(Callable).This default implementation, used only by a third-party operator that does not override it, runs the action directly without a transaction.
- Specified by:
transactionin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
action- the operations to execute
-
insertAll
Insert multiple entities atomically: on every shipped backend either all of them are stored or, if one fails, none is. Uses JDBC batch for SQL backends; on JSON the batch is one transaction. Inside an active transaction the batch joins it.Atomic: runs as one transaction (joining the one active on this thread, if any), so a failure part-way stores none of the entities. The batch goes through
transaction(Runnable), so a batch transaction it begins locks a table at its first read; every insert takes the table's lock before its own read, so this changes nothing here.- Specified by:
insertAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entities- the entities to insert
-
updateAll
Update multiple entities atomically: on every shipped backend either all of them are updated or, if one fails, none is. Uses JDBC batch for SQL backends; on JSON the batch is one transaction. Inside an active transaction the batch joins it.This default implementation, used only by a third-party operator that does not override it, updates the entities one by one without a transaction.
Atomic: runs as one transaction (joining the one active on this thread, if any), so a failure part-way changes none of the entries. The batch goes through
transaction(Callable), so a batch transaction it begins locks a table at its first read; every update takes the table's lock before its own read, so this changes nothing here.- Specified by:
updateAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entities- the entities to update- Throws:
IllegalAccessException- if field access fails
-
conditionCal
-
stripJsonQuotes
Remove JSON quotes from a serialized string value. e.g., "\"hello\"" becomes "hello"
-