Class AbstractRelationalDataOperator<T extends BaseDataEntity<String>>

java.lang.Object
com.ultikits.ultitools.interfaces.impl.data.AbstractRelationalDataOperator<T>
Type Parameters:
T - the entity type
All Implemented Interfaces:
DataOperator<T>
Direct Known Subclasses:
MysqlDataOperator, SQLiteDataOperator

public abstract class AbstractRelationalDataOperator<T extends BaseDataEntity<String>> extends Object implements DataOperator<T>
Abstract base class for relational database data operators.

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
  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • GSON

      private static final com.google.gson.Gson GSON
      Default Gson has no bundled adapter for java.time.LocalDateTime: its reflective fallback tries to reach LocalDateTime's private fields, which JDK 9+'s module system refuses without --add-opens java.base/java.time, throwing JsonIOException at the first non-null @Column-mapped LocalDateTime value (02-08 -- AuditableDataEntity#createdAt/updatedAt were always null before this plan, so the per-field insert(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/from LocalDateTime.toString()'s ISO-8601 form, which LocalDateTime.parse(CharSequence) reads back exactly.
    • type

      protected final Class<T extends BaseDataEntity<String>> type
    • dataSource

      protected final DataSource dataSource
    • tableName

      protected final String tableName
    • queryRunner

      protected final org.apache.commons.dbutils.QueryRunner queryRunner
    • transactionManager

      protected TransactionManager transactionManager
    • knownColumns

      private final Set<String> knownColumns
      The entity's own @Column names, reflected once at construction. The allow-list validateColumn(String) checks every WHERE-clause column identifier against before it is concatenated into SQL text (T-02-SQLI-1).
  • Constructor Details

    • AbstractRelationalDataOperator

      protected AbstractRelationalDataOperator(DataSource dataSource, Class<T> type)
      Creates a new data operator.
      Parameters:
      dataSource - the data source to use
      type - the entity class
  • Method Details

    • buildKnownColumns

      private static Set<String> buildKnownColumns(Class<?> entityType)
      Reflects the entity's @Column mappings once, for validateColumn(String)'s allow-list. This mirrors the same reflected metadata buildColumnDefinitions(Class) and getColumnMappings() 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 @Column SQL name declared on entityType (including inherited fields, e.g. BaseDataEntity's id)
    • validateColumn

      private void validateColumn(String column)
      Refuses a column identifier that is not among the entity's own reflected @Column mappings, before it can be concatenated into SQL text (T-02-SQLI-1). Called from appendConditions(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-supplied WhereCondition
      Throws:
      DataAccessException - if column is not a known column of type
    • setTransactionManager

      public void setTransactionManager(TransactionManager transactionManager)
      Sets the transaction manager for transaction-aware operations.
      Parameters:
      transactionManager - the transaction manager
    • currentTimeoutDeadlineNanos

      private Long currentTimeoutDeadlineNanos()
      Supplies queryRunner's per-statement query timeout deadline (D-10): the active transaction's remaining-time budget, or null when none is configured (no active transaction, or @Transactional(timeout=0), the default). Also used directly by applyStatementTimeout(java.sql.PreparedStatement) for the two PreparedStatement call sites that bypass queryRunner entirely. A method reference rather than a field capture, so every call reads transactionManager's *current* value -- this operator outlives any single transaction, and setTransactionManager(com.ultikits.ultitools.interfaces.TransactionManager) can swap the manager out.
      Returns:
      the active transaction's timeout deadline, or null if none is configured
    • applyStatementTimeout

      private void applyStatementTimeout(PreparedStatement statement) throws SQLException
      Applies the same per-statement query timeout queryRunner (a TimeoutAwareQueryRunner) would, to a PreparedStatement built directly against dataSource -- the two call sites in insertAll(java.util.List<T>) and updateAll(java.util.List<T>) that bypass QueryRunner entirely. A timeout wired only into queryRunner'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

      protected org.apache.commons.dbutils.ResultSetHandler<List<T>> getListHandler()
    • getRawListHandler

      private org.apache.commons.dbutils.ResultSetHandler<List<T>> getRawListHandler()
      The same row-materialization logic as getListHandler(), without firing onLoad(). Used internally by getById(java.lang.Object) (which fires onLoad() itself, exactly once, on the single entity it returns) and delById(java.lang.Object) (which needs the entity to fire onDelete() on, but deleting is not loading -- see 02-08-PLAN.md's delete-hook-path split).
    • fetchRawById

      private T fetchRawById(Object id)
      Fetches a single row by id without firing onLoad(). Shared by getById(java.lang.Object) (which fires onLoad() on the result) and delById(java.lang.Object) (which fires onDelete() instead -- deleting is not loading).
    • sqlOperatorFor

      private String sqlOperatorFor(Comparison comparison)
      Maps a Comparison to 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 given Comparison means the same thing on every one of them, and on SimpleJsonDataOperator's JSON backend. There is no silent fallback to equality: an unhandled constant throws rather than being misread as EQUAL, since a silent default is exactly how this defect stayed invisible before.

      Parameters:
      comparison - the comparison operator
      Returns:
      the SQL fragment, e.g. " > ?"
    • likeWrappedValue

      private Object likeWrappedValue(WhereCondition condition)
      Wraps a condition's value with the % wildcards its Comparison implies, matching SimpleJsonDataOperator#conditionCal's existing placement exactly: INCLUDE wraps both sides, STARTSWITH appends a trailing wildcard, ENDSWITH prepends a leading wildcard. EQUAL/GREATER/LESS pass 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 a WHERE clause built from conditions to sql and their bound values to params, routing every column through sqlOperatorFor(com.ultikits.ultitools.entities.Comparison) so the four relational builders cannot diverge in how they honor a Comparison again.

      When skipEmpty is true, conditions whose WhereCondition#isEmpty() is true are skipped — getAll(WhereCondition...)'s pre-existing behavior, preserved here rather than changed. exist(WhereCondition...), page(int, int, WhereCondition...), and del(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 clause
      params - the parameter list to append bound values to, in SQL order
      conditions - the conditions to render; must be non-null and non-empty
      skipEmpty - whether to skip conditions whose isEmpty() is true
    • exist

      public boolean exist(T object)
      Description copied from interface: DataOperator
      Check if the data record exists.
      Specified by:
      exist in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      object - Data record entity
      Returns:
      Whether the record exists
    • exist

      public boolean exist(WhereCondition... whereConditions)
      Description copied from interface: DataOperator
      Check if the data record exists.
      Specified by:
      exist in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
      Returns:
      Whether the record exists
    • getById

      public T getById(Object id)
      Description copied from interface: DataOperator
      Get data record by ID.
      Specified by:
      getById in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      id - Record ID
      Returns:
      Data record
    • getAll

      public List<T> getAll()
      Description copied from interface: DataOperator
      Get all data record.
      Specified by:
      getAll in interface DataOperator<T extends BaseDataEntity<String>>
      Returns:
      Data record list
    • getAll

      public List<T> getAll(WhereCondition... whereConditions)
      Description copied from interface: DataOperator
      Get all data record by conditions.
      Specified by:
      getAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
      Returns:
      Data record list
    • getLike

      public List<T> getLike(String column, String value, DataOperator.LikeType likeType)
      Description copied from interface: DataOperator
      Fuzzy Query
      Specified by:
      getLike in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      column - Column name
      value - Query value
      likeType - Like type
      Returns:
      Data record list
    • page

      public List<T> page(int page, int size, WhereCondition... whereConditions)
      Description copied from interface: DataOperator
      Get data record by page.
      Specified by:
      page in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      page - Page number
      size - Page size
      whereConditions - Conditions
      Returns:
      Data record list
    • insert

      public void insert(T obj)
      Description copied from interface: DataOperator
      Insert data record.
      Specified by:
      insert in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      obj - Data record
    • del

      public void del(WhereCondition... whereConditions)
      Deletes by predicate, without loading matched rows into entities first. Consequently this overload does not fire onDelete() -- 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 fire onDelete() on it (02-08-PLAN.md's delete-hook-path split).
      Specified by:
      del in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
    • delById

      public void delById(Object id)
      Deletes by id, fetching the entity first so onDelete() can fire on it -- unlike del(WhereCondition...), which deletes by predicate without materializing rows and therefore does not fire the hook. If no row matches id, nothing is fetched and onDelete() does not fire.
      Specified by:
      delById in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      id - Record ID
    • update

      public void update(String column, Object value, Object id)
      Description copied from interface: DataOperator
      Update one field of one record.
      Specified by:
      update in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      column - Column name
      value - New value
      id - Record ID
    • update

      public void update(T obj) throws IllegalAccessException
      Description copied from interface: DataOperator
      Update data record by object. It will not update the fields that the incoming entity does not have.
      Specified by:
      update in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      obj - Data record
      Throws:
      IllegalAccessException - Please referIllegalAccessException
    • transaction

      public <R> R transaction(Callable<R> action) throws Exception
      Description copied from interface: DataOperator
      Execute 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:
      transaction in interface DataOperator<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

      public void transaction(Runnable action)
      Description copied from interface: DataOperator
      Execute operations within a transaction (void variant).
      Specified by:
      transaction in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      action - the operations to execute
    • insertAll

      public void insertAll(List<T> entities)
      Description copied from interface: DataOperator
      Insert multiple entities atomically. Uses JDBC batch for SQL backends.
      Specified by:
      insertAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entities - the entities to insert
    • updateAll

      public void updateAll(List<T> entities) throws IllegalAccessException
      Description copied from interface: DataOperator
      Update multiple entities atomically. Uses JDBC batch for SQL backends.
      Specified by:
      updateAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entities - the entities to update
      Throws:
      IllegalAccessException - if field access fails
    • getColumnMappings

      private List<AbstractRelationalDataOperator.ColumnMapping> getColumnMappings()
    • createTableSqlFromClazz

      protected abstract String createTableSqlFromClazz(Class<T> type)
      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

      private static Object normaliseBoolean(Object value, String colName)
      Normalises whatever the driver returned for a boolean column into a real Boolean, 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 is VARCHAR(255) and buildColumnDefinitions(java.lang.Class<T>) emits it verbatim, so a boolean field with no explicit type gets a text column. rs.getObject then returns a String, the Number branch was skipped, and Gson coerced the JSON string "1" to false -- 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, /unban reporting "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 boolean type returns a Number or Boolean. 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 becoming false is precisely the behaviour this method exists to remove.

      Parameters:
      value - whatever ResultSet.getObject returned
      colName - the column name, for the diagnostic
      Returns:
      null, or a Boolean
      Since:
      6.3.0
    • buildColumnDefinitions

      protected String buildColumnDefinitions(Class<T> type)
      Builds the column definitions portion of CREATE TABLE.