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.
134 lines
7.8 KiB
Markdown
134 lines
7.8 KiB
Markdown
# 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](architecture.md#startup-lifecycle):
|
|
|
|
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:
|
|
|
|
```java
|
|
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`](utilities-and-bootstrap.md) (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](tasks.md#31-persistable-tasks) 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`](events.md) and
|
|
[`LogHandler`](utilities-and-bootstrap.md#loghandler).
|
|
- **`BlacklistService`** — maintains a `blacklistCache` (guild id → set of words) for fast message
|
|
filtering.
|
|
- **`GuildSettingService`** — reads/writes guild settings backed by [`GSettings`](#gsettings); caches
|
|
values in `settingsCache` and reloads on change; used by the
|
|
[web API settings endpoints](web-api.md).
|
|
- **`TaskService`** — serializes [`PersistableTask`](tasks.md#31-persistable-tasks) instances to JSON
|
|
for the `tasks` table, keyed by the task's stable `typeId()` in `task_class`, and reconstructs them
|
|
at startup via [`PersistableTaskRegistry`](tasks.md#persistabletaskregistry--discovery-and-restore).
|
|
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.
|