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.
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:
-
SessionManager.getSessionFactory()— lazily builds the HibernateSessionFactory. It reads the static config fromhibernate.cfg.xml(dialectMySQLDialect, drivercom.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); -
DatabaseMigration.migrate()— runsflyway.migrate()against the same database, applying the versioned scripts undersrc/main/resources/db/migration/(starting withV1__initial_schema.sql). The Flyway Maven plugin (flyway-dev.sh/.ps1) is available for local development, usingFLYWAY_URL/FLYWAY_USER/FLYWAY_PASSWORD. -
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)(viasession.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 forOTaskrows (used by the task persistence flow).
Service layer
Services wrap repositories with business logic and caching.
GuildService— gateway for guild operations; caches oneGuildServiceper guild id (ConcurrentHashMap);findOrCreateguarantees a guild row exists;deleteGuild()on leave;isDebugEnabled()/ debug-channel helpers used byEventsHandlerandLogHandler.BlacklistService— maintains ablacklistCache(guild id → set of words) for fast message filtering.GuildSettingService— reads/writes guild settings backed byGSettings; caches values insettingsCacheand reloads on change; used by the web API settings endpoints.TaskService— serializesPersistableTaskinstances to JSON for thetaskstable, keyed by the task's stabletypeId()intask_class, and reconstructs them at startup viaPersistableTaskRegistry. Rows with an unknowntask_classare 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.