Files
netneutrality/docs/persistence.md
2026-10-19 08:21:42 -04:00

7.8 KiB

Persistence & Database

Packages: me.bobert.netneutrality.db (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.db.entities 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.