# 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`** (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.