Files
netneutrality/docs/persistence.md
T
anthorob 05b4a2ab85 Refactor persistable-task system with self-registering tasks
Replaced metadata-driven reflection with self-describing `PersistableTask` types and a new `PersistableTaskRegistry`. Removed `persistable-tasks.json` and `TaskRegistry`, migrating to type-safe DI instantiation and explicit `restore()` methods. Fixed task restoration issues, ensuring `EmojiReactionRoleTask` and its `emojiMapping` now persist and restore correctly. Introduced `typeId()` for stable task identification, updated payload serialization for symmetry, and applied database migration to map FQCNs to logical IDs. Added unit tests for robust type discovery, payload symmetry, and registry behavior, ensuring no breaking changes.
2026-09-25 07:35:45 -04:00

7.8 KiB

Persistence & Database

Packages: me.bobert.netneutrality.platform.persistence (session/migration), me.bobert.netneutrality.db (entities, repositories, services). Resources: hibernate.cfg.xml, db/migration/V1__initial_schema.sql, db/migration/V2__task_type_id.sql.

Stack

Concern Technology
ORM Hibernate 7.1.9 (Jakarta JPA)
Engine MariaDB 11 (per docker-compose.yml), MySQL Connector/J 9.7.0 driver, InnoDB
Pooling HikariCP (via hibernate-hikaricp)
Migrations Flyway 11.20.3 (flyway-core + flyway-mysql)

Configuration is externalized via Settings (env vars / bot.properties): MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE.

Session & migration lifecycle

Both are wired in at startup:

  1. SessionManager.getSessionFactory() — lazily builds the Hibernate SessionFactory. It reads the static config from hibernate.cfg.xml (dialect MySQLDialect, driver com.mysql.cj.jdbc.Driver, the Hikari connection provider, and the mapped entity list) and overrides the JDBC url/user/password from environment variables:

    settings.put(Environment.JAKARTA_JDBC_URL,
        String.format("jdbc:mysql://%s:%s/%s", dbHost, dbPort, dbName));
    settings.put(Environment.JAKARTA_JDBC_USER, dbUser);
    settings.put(Environment.JAKARTA_JDBC_PASSWORD, dbPass);
    
  2. DatabaseMigration.migrate() — runs flyway.migrate() against the same database, applying the versioned scripts under src/main/resources/db/migration/ (starting with V1__initial_schema.sql). The Flyway Maven plugin (flyway-dev.sh/.ps1) is available for local development, using FLYWAY_URL / FLYWAY_USER / FLYWAY_PASSWORD.

  3. SessionManager.shutdown() — destroys the service registry / releases the pool on shutdown.

Repositories obtain sessions through the factory; transactional access uses helpers in JpaUtils (open session, run in a transaction, close).

Entity catalog

Entities live in me.bobert.netneutrality.data.entity and are prefixed with O. OGuild is the hub of a star-shaped model; most other tables reference guild_id with ON DELETE CASCADE.

Entity Table Key Relationships / notes
OGuild guilds guild_id (String) Central entity; one-to-many with settings, blacklist, log messages, logging.
OGuildSetting guild_settings id (auto int) Many-to-one OGuild; key/value guild settings.
OBlacklist blacklist id (auto int) Many-to-one OGuild; blacklisted words.
OLogging logging OLoggingId (composite) Composite key guild_id + channel_id; @MapsId to OGuild; debug/error log channel config.
OLoggingId — (embeddable) — @Embeddable/@IdClass for the composite key.
OLogMessage log_message id (auto int) Many-to-one OGuild; message logging history.
OTask tasks id (auto int) Persistable task state; unique task_id, task_class holds the logical typeId, JSON payload.
OWebUser web_users id (String) Web dashboard user (tokens/expiry). Present in schema & code but not listed in hibernate.cfg.xml.

ER overview

                    ┌───────────────┐
                    │    OGuild     │  (guilds.guild_id)
                    └───────┬───────┘
        ┌───────────┬───────┼────────────┬─────────────┐
        ▼           ▼       ▼            ▼             ▼
 OGuildSetting  OBlacklist  OLogging   OLogMessage   (…)
 guild_settings  blacklist  logging    log_message
   (FK guild)    (FK guild) (PK guild+ (FK guild)
                             channel)

  OTask (tasks)          OWebUser (web_users)     ── standalone

OTask and OWebUser are not tied to the guild graph.

Repository layer

A lightweight generic repository pattern is used.

  • Repository<O> (interface) — generic CRUD: findAll(), findById(String), save(O) (via session.merge()), deleteBy(String).
  • BlacklistRepository — find-by-guild and case-insensitive word lookups.
  • GuildRepository — CRUD for guilds.
  • GuildSettingRepository — set/get setting values; removes rows that equal the default value.
  • MessageRepository — message logging / edit history.
  • TaskRepository — CRUD for OTask rows (used by the task persistence flow).

Service layer

Services wrap repositories with business logic and caching.

  • GuildService — gateway for guild operations; caches one GuildService per guild id (ConcurrentHashMap); findOrCreate guarantees a guild row exists; deleteGuild() on leave; isDebugEnabled() / debug-channel helpers used by EventsHandler and LogHandler.
  • BlacklistService — maintains a blacklistCache (guild id → set of words) for fast message filtering.
  • GuildSettingService — reads/writes guild settings backed by GSettings; caches values in settingsCache and reloads on change; used by the web API settings endpoints.
  • TaskService — serializes PersistableTask instances to JSON for the tasks table, keyed by the task's stable typeId() in task_class, and reconstructs them at startup via PersistableTaskRegistry. Rows with an unknown task_class are logged and skipped.

GSettings

GSettings is an enum of the available per-guild configuration keys and their defaults:

Setting Default
MUSIC_ROLE (none)
MUSIC_CHANNEL (none)
MUSIC_VOLUME 30
MUSIC_MAXPLAYLISTSIZE 20

GuildSettingService looks up an OGuildSetting row by the enum name(); if no row exists for a guild/setting, the enum's defaultValue is used. This is how MusicCommand obtains the guild's music role/channel and how ConfigureCommand persists changes.

Schema (V1__initial_schema.sql)

The initial migration creates the tables backing the entities above (guilds, guild_settings, blacklist, logging, log_message, tasks, web_users), with guild_id foreign keys and ON DELETE CASCADE from the guild-scoped tables to guilds. Subsequent schema changes should be added as new V3__…, V4__… Flyway scripts in the same directory.

V2__task_type_id.sql rewrites tasks.task_class from the old fully-qualified class names to the stable logical typeId (e.g. emoji-reaction-role), so previously persisted rows remain loadable.