Class AbstractRelationalDataOperator<T extends BaseDataEntity<String>>
- Type Parameters:
T- the entity type
- All Implemented Interfaces:
DataOperator<T>
- Direct Known Subclasses:
MysqlDataOperator,SQLiteDataOperator
This class extracts common logic from MySQL and SQLite implementations, providing a unified implementation for CRUD operations. Only the database-specific SQL dialect differences need to be overridden.
- Since:
- 6.2.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static classNested classes/interfaces inherited from interface com.ultikits.ultitools.interfaces.DataOperator
DataOperator.LikeType -
Field Summary
FieldsModifier and TypeFieldDescriptionprotected final DataSourceprivate 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, throwingJsonIOExceptionat the first non-null@Column-mappedLocalDateTimevalue (02-08 --AuditableDataEntity#createdAt/updatedAtwere alwaysnullbefore this plan, so the per-fieldinsert(T)/update(java.lang.String, java.lang.Object, java.lang.Object)JSON fallback below never reached this type until the lifecycle hooks started populating it).The entity's own@Columnnames, reflected once at construction.private static final Loggerprotected final org.apache.commons.dbutils.QueryRunnerprotected final Stringprotected TransactionManager -
Constructor Summary
ConstructorsModifierConstructorDescriptionprotectedAbstractRelationalDataOperator(DataSource dataSource, Class<T> type) Creates a new data operator. -
Method Summary
Modifier and TypeMethodDescriptionprivate voidappendConditions(StringBuilder sql, List<Object> params, WhereCondition[] conditions, boolean skipEmpty) Appends aWHEREclause built fromconditionstosqland their bound values toparams, routing every column throughsqlOperatorFor(com.ultikits.ultitools.entities.Comparison)so the four relational builders cannot diverge in how they honor aComparisonagain.private voidapplyStatementTimeout(PreparedStatement statement) Applies the same per-statement query timeoutqueryRunner(aTimeoutAwareQueryRunner) would, to aPreparedStatementbuilt directly againstdataSource-- the two call sites ininsertAll(java.util.List<T>)andupdateAll(java.util.List<T>)that bypassQueryRunnerentirely.protected StringbuildColumnDefinitions(Class<T> type) Builds the column definitions portion of CREATE TABLE.buildKnownColumns(Class<?> entityType) Reflects the entity's@Columnmappings once, forvalidateColumn(String)'s allow-list.protected abstract StringcreateTableSqlFromClazz(Class<T> type) Creates the SQL statement to create the table.private LongSuppliesqueryRunner's per-statement query timeout deadline (D-10): the active transaction's remaining-time budget, ornullwhen none is configured (no active transaction, or@Transactional(timeout=0), the default).voiddel(WhereCondition... whereConditions) Deletes by predicate, without loading matched rows into entities first.voidDeletes by id, fetching the entity first soonDelete()can fire on it -- unlikedel(WhereCondition...), which deletes by predicate without materializing rows and therefore does not fire the hook.booleanexist(WhereCondition... whereConditions) Check if the data record exists.booleanCheck if the data record exists.private TfetchRawById(Object id) Fetches a single row by id without firingonLoad().getAll()Get all data record.getAll(WhereCondition... whereConditions) Get all data record by conditions.Get data record by ID.getLike(String column, String value, DataOperator.LikeType likeType) Fuzzy QueryThe read handler used bygetAll(WhereCondition...),getLike(java.lang.String, java.lang.String, com.ultikits.ultitools.interfaces.DataOperator.LikeType), andpage(int, int, com.ultikits.ultitools.entities.WhereCondition...): firesonLoad()once per row it materializes.The same row-materialization logic asgetListHandler(), without firingonLoad().private voidInitializes the database table.voidInsert data record.voidInsert multiple entities atomically.private ObjectlikeWrappedValue(WhereCondition condition) Wraps a condition's value with the%wildcards itsComparisonimplies, matchingSimpleJsonDataOperator#conditionCal's existing placement exactly:INCLUDEwraps both sides,STARTSWITHappends a trailing wildcard,ENDSWITHprepends a leading wildcard.private static ObjectnormaliseBoolean(Object value, String colName) Normalises whatever the driver returned for abooleancolumn into a realBoolean, so Gson deserialises it correctly.page(int page, int size, WhereCondition... whereConditions) Get data record by page.voidsetTransactionManager(TransactionManager transactionManager) Sets the transaction manager for transaction-aware operations.private StringsqlOperatorFor(Comparison comparison) Maps aComparisonto its SQL operator fragment (including the placeholder).voidtransaction(Runnable action) Execute operations within a transaction (void variant).<R> Rtransaction(Callable<R> action) Execute operations within a transaction.voidUpdate one field of one record.voidUpdate data record by object.voidUpdate multiple entities atomically.private voidvalidateColumn(String column) Refuses a column identifier that is not among the entity's own reflected@Columnmappings, before it can be concatenated into SQL text (T-02-SQLI-1).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, throwingJsonIOExceptionat the first non-null@Column-mappedLocalDateTimevalue (02-08 --AuditableDataEntity#createdAt/updatedAtwere alwaysnullbefore this plan, so the per-fieldinsert(T)/update(java.lang.String, java.lang.Object, java.lang.Object)JSON fallback below never reached this type until the lifecycle hooks started populating it). Serializes to/fromLocalDateTime.toString()'s ISO-8601 form, whichLocalDateTime.parse(CharSequence)reads back exactly. -
type
-
dataSource
-
tableName
-
queryRunner
protected final org.apache.commons.dbutils.QueryRunner queryRunner -
transactionManager
-
knownColumns
The entity's own@Columnnames, reflected once at construction. The allow-listvalidateColumn(String)checks every WHERE-clause column identifier against before it is concatenated into SQL text (T-02-SQLI-1).
-
-
Constructor Details
-
AbstractRelationalDataOperator
Creates a new data operator.- Parameters:
dataSource- the data source to usetype- the entity class
-
-
Method Details
-
buildKnownColumns
Reflects the entity's@Columnmappings once, forvalidateColumn(String)'s allow-list. This mirrors the same reflected metadatabuildColumnDefinitions(Class)andgetColumnMappings()already read from the entity class — a dedicated pass over it, not a second scan of anything already cached.- Parameters:
entityType- the entity class- Returns:
- an unmodifiable set of every
@ColumnSQL name declared onentityType(including inherited fields, e.g.BaseDataEntity'sid)
-
validateColumn
Refuses a column identifier that is not among the entity's own reflected@Columnmappings, before it can be concatenated into SQL text (T-02-SQLI-1). Called fromappendConditions(java.lang.StringBuilder, java.util.List<java.lang.Object>, com.ultikits.ultitools.entities.WhereCondition[], boolean)on behalf of all four relational WHERE builders.- Parameters:
column- the column name from a caller-suppliedWhereCondition- Throws:
DataAccessException- ifcolumnis not a known column oftype
-
setTransactionManager
Sets the transaction manager for transaction-aware operations.- Parameters:
transactionManager- the transaction manager
-
currentTimeoutDeadlineNanos
SuppliesqueryRunner's per-statement query timeout deadline (D-10): the active transaction's remaining-time budget, ornullwhen none is configured (no active transaction, or@Transactional(timeout=0), the default). Also used directly byapplyStatementTimeout(java.sql.PreparedStatement)for the twoPreparedStatementcall sites that bypassqueryRunnerentirely. A method reference rather than a field capture, so every call readstransactionManager's *current* value -- this operator outlives any single transaction, andsetTransactionManager(com.ultikits.ultitools.interfaces.TransactionManager)can swap the manager out.- Returns:
- the active transaction's timeout deadline, or
nullif none is configured
-
applyStatementTimeout
Applies the same per-statement query timeoutqueryRunner(aTimeoutAwareQueryRunner) would, to aPreparedStatementbuilt directly againstdataSource-- the two call sites ininsertAll(java.util.List<T>)andupdateAll(java.util.List<T>)that bypassQueryRunnerentirely. A timeout wired only intoqueryRunner's path would leave these two batch operations silently ignoring@Transactional(timeout=), exactly the gap 02-RESEARCH.md warns about.- Parameters:
statement- the statement to apply the timeout to, already prepared- Throws:
SQLException- if the driver rejects the timeout value
-
initializeTable
private void initializeTable()Initializes the database table. -
getListHandler
The read handler used bygetAll(WhereCondition...),getLike(java.lang.String, java.lang.String, com.ultikits.ultitools.interfaces.DataOperator.LikeType), andpage(int, int, com.ultikits.ultitools.entities.WhereCondition...): firesonLoad()once per row it materializes.getById(java.lang.Object)anddelById(java.lang.Object)deliberately do *not* go through this handler -- seegetRawListHandler(). -
getRawListHandler
The same row-materialization logic asgetListHandler(), without firingonLoad(). Used internally bygetById(java.lang.Object)(which firesonLoad()itself, exactly once, on the single entity it returns) anddelById(java.lang.Object)(which needs the entity to fireonDelete()on, but deleting is not loading -- see 02-08-PLAN.md's delete-hook-path split). -
fetchRawById
Fetches a single row by id without firingonLoad(). Shared bygetById(java.lang.Object)(which firesonLoad()on the result) anddelById(java.lang.Object)(which firesonDelete()instead -- deleting is not loading). -
sqlOperatorFor
Maps aComparisonto its SQL operator fragment (including the placeholder).Shared by all four relational WHERE builders (
exist(WhereCondition...),getAll(WhereCondition...),page(int, int, WhereCondition...),del(WhereCondition...)) so a givenComparisonmeans the same thing on every one of them, and onSimpleJsonDataOperator's JSON backend. There is no silent fallback to equality: an unhandled constant throws rather than being misread asEQUAL, since a silent default is exactly how this defect stayed invisible before.- Parameters:
comparison- the comparison operator- Returns:
- the SQL fragment, e.g.
" > ?"
-
likeWrappedValue
Wraps a condition's value with the%wildcards itsComparisonimplies, matchingSimpleJsonDataOperator#conditionCal's existing placement exactly:INCLUDEwraps both sides,STARTSWITHappends a trailing wildcard,ENDSWITHprepends a leading wildcard.EQUAL/GREATER/LESSpass the value through unchanged.- Parameters:
condition- the condition supplying the comparison and the raw value- Returns:
- the value to bind as the SQL parameter
-
appendConditions
private void appendConditions(StringBuilder sql, List<Object> params, WhereCondition[] conditions, boolean skipEmpty) Appends aWHEREclause built fromconditionstosqland their bound values toparams, routing every column throughsqlOperatorFor(com.ultikits.ultitools.entities.Comparison)so the four relational builders cannot diverge in how they honor aComparisonagain.When
skipEmptyistrue, conditions whoseWhereCondition#isEmpty()istrueare skipped —getAll(WhereCondition...)'s pre-existing behavior, preserved here rather than changed.exist(WhereCondition...),page(int, int, WhereCondition...), anddel(WhereCondition...)do not skip empty conditions; that asymmetry already existed before this method and is not resolved by this change (see 02-03-SUMMARY.md).- Parameters:
sql- the SQL being built; must already hold the statement up to (not including) the WHERE clauseparams- the parameter list to append bound values to, in SQL orderconditions- the conditions to render; must be non-null and non-emptyskipEmpty- whether to skip conditions whoseisEmpty()is true
-
exist
Description copied from interface:DataOperatorCheck if the data record exists.- 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
Description copied from interface:DataOperatorGet all data record by conditions.- Specified by:
getAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
whereConditions- Conditions- Returns:
- Data record list
-
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
Description copied from interface:DataOperatorInsert data record.- Specified by:
insertin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
obj- Data record
-
del
Deletes by predicate, without loading matched rows 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).- Specified by:
delin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
whereConditions- Conditions
-
delById
Deletes by id, fetching the entity first soonDelete()can fire on it -- unlikedel(WhereCondition...), which deletes by predicate without materializing rows and therefore does not fire the hook. If no row matchesid, nothing is fetched andonDelete()does not fire.- Specified by:
delByIdin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
id- Record ID
-
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- Throws:
IllegalAccessException- Please referIllegalAccessException
-
transaction
Description copied from interface:DataOperatorExecute operations within a transaction. All operations commit together or roll back on exception. For SQL backends, uses database transactions. For JSON, uses snapshot-based rollback.- 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).- Specified by:
transactionin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
action- the operations to execute
-
insertAll
Description copied from interface:DataOperatorInsert multiple entities atomically. Uses JDBC batch for SQL backends.- Specified by:
insertAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entities- the entities to insert
-
updateAll
Description copied from interface:DataOperatorUpdate multiple entities atomically. Uses JDBC batch for SQL backends.- Specified by:
updateAllin interfaceDataOperator<T extends BaseDataEntity<String>>- Parameters:
entities- the entities to update- Throws:
IllegalAccessException- if field access fails
-
getColumnMappings
-
createTableSqlFromClazz
Creates the SQL statement to create the table.Override this method to customize table creation for different databases.
- Parameters:
type- the entity class- Returns:
- the CREATE TABLE SQL statement
-
normaliseBoolean
Normalises whatever the driver returned for abooleancolumn into a realBoolean, so Gson deserialises it correctly.The previous version converted only
Number, on the stated assumption that "SQLite stores BOOLEAN as INTEGER (0/1)". That assumption does not hold here, because of what this class writes:@Column's default type isVARCHAR(255)andbuildColumnDefinitions(java.lang.Class<T>)emits it verbatim, so abooleanfield with no explicittypegets a text column.rs.getObjectthen returns aString, theNumberbranch was skipped, and Gson coerced the JSON string"1"tofalse-- every defaulted boolean column read back false regardless of what was stored (#388). It was one root cause behind a banned player still being able to join,/unbanreporting "not banned" for a player it had just banned, and a world's weather flag having no effect.Both representations are handled rather than one, because both exist in the field: tables created before this fix have text columns holding
"1"/"0", and a column declared with an explicit numeric or booleantypereturns aNumberorBoolean. The column type is deliberately left as it is -- correcting it would change the schema for new installs only, would not help the tables that already exist, and reading both forms is required either way.An unrecognised value is logged rather than quietly becoming
false. Quietly becomingfalseis precisely the behaviour this method exists to remove.- Parameters:
value- whateverResultSet.getObjectreturnedcolName- the column name, for the diagnostic- Returns:
null, or aBoolean- Since:
- 6.3.0
-
buildColumnDefinitions
Builds the column definitions portion of CREATE TABLE.
-