129 lines
7.4 KiB
Markdown
129 lines
7.4 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`.
|
|
|
|
## 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`, class name, 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 and reconstructs them at startup via
|
|
[`TaskRegistry`/`TaskFactory`](tasks.md#31-persistable-tasks).
|
|
|
|
## 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 `V2__…`, `V3__…` Flyway scripts in the same directory.
|