--- sessionId: session-260925-095225-4rh7 --- # Requirements ### Overview & Goals Swap the hand-written HQL in `db/repository/*` for **Jakarta Data 1.0** repository interfaces. Hibernate's `hibernate-processor` generates the implementations at compile time. Queries become compile-time checked and the boilerplate goes away. The public repository API used by `App`, services and tests stays the same. ### Scope **In scope** - Build setup: `jakarta.data-api` plus `hibernate-processor` as an annotation processor. - Generated query interfaces for `OGuild`, `OGuildSetting`, `OTask`, `OBlacklist` and `OLogMessage`. - The existing repository classes become thin transactional facades over the generated code. - `AbstractRepository` is removed. **Out of scope** - Moving to Spring or CDI. - Schema or Flyway changes. - Changing any service behaviour. ### Functional Requirements - Every current public method keeps the same signature and behaviour. This includes `findOrCreate`, `upsertOrDelete`, `upsert`, `enableWord`, `deleteWord`, `deleteByTaskId`, `findEnabledWords`, `findAll`, `findById`, `save` and `deleteById`. - Each read-modify-write operation still runs in a single transaction. On error the transaction rolls back and the exception is rethrown. - An invalid query must fail `mvn compile`. # Technical Design ### Current Implementation - `AbstractRepository` gives each repository `read`/`write`/`writeVoid` helpers over a stateful `Session`, using `SessionFactory.fromSession`/`fromTransaction`. - The repositories build HQL inline (for example `TaskRepository.findByTaskId`) or use named queries (`settings_findByName`). - They are wired in `App.java` (lines 122-124 and 138) as `new XRepository(sessions)`. Tests construct them the same way. ### Key Decisions 1. **Generated interfaces run on a `StatelessSession`.** This is the Hibernate Data default: the generated `*_` class takes a `StatelessSession` in its constructor. There's no container, so transactions stay our responsibility. 2. **The facade pattern keeps the public API.** Classes like `GuildSettingRepository` stay as the public API. Each call opens a stateless transaction and creates the generated DAO. `App`, the services and the tests don't change. 3. **No dirty checking, so writes are explicit.** Code that changes a loaded entity must call `@Update`. New rows are written with `@Insert`, and removals use `@Delete` or a `@Query("delete ...")`. `session.getReference(OGuild…)` becomes a detached `OGuild` with only its id set, which is enough for the foreign key on insert. ### Proposed Changes - `db/TransactionRunner.java` (new) replaces `AbstractRepository`. It exposes ` R inTx(Function)` using `SessionFactory.fromStatelessTransaction`, and `read(...)` using `fromStatelessSession`. - `db/data/*Queries.java` (new) holds the `@Repository` interfaces, for example: ```java @Repository public interface TaskQueries extends CrudRepository { @Find Optional byTaskId(String taskId); @Query("delete from OTask where taskId = :taskId") int deleteByTaskId(String taskId); } @Repository public interface GuildSettingQueries extends CrudRepository { @Query("where guild.guildId = :guildId") List byGuild(String guildId); @Query("where guild.guildId = :id and name = :name") List byName(String id, String name); } ``` - Repository facades, for example `upsertOrDelete`: ```java tx.inTx(ss -> { var q = new GuildSettingQueries_(ss); var ex = q.byName(id, s.getName()); if (reset) { q.deleteAll(ex); return null; } if (ex.isEmpty()) q.insert(newSetting(id, s, value)); else { ex.forEach(e -> e.setValue(value)); q.updateAll(ex); } return null; }); ``` - `BlacklistQueries` covers `byGuild` (ordered by id), `enabledWords` (a `select o.word` projection) and `findWord` (case-insensitive `upper()`). - `MessageQueries` holds the same kind of migration for `MessageRepository`. - `pom.xml` gets: - `jakarta.data:jakarta.data-api:1.0.1` - `maven-compiler-plugin` with `annotationProcessorPaths` pointing to `org.hibernate.orm:hibernate-processor:7.1.9.Final` - The shade plugin keeps working unchanged. ### Risks - **Lazy associations:** stateless sessions don't lazy-load outside the session. The facades must return fully initialised data, which the current code already assumes since its sessions are closed anyway. - **`save()` semantics:** `merge` becomes `upsert`, available on `CrudRepository`. - **Named query `settings_findByName`:** it's replaced by `@Query`. The `@NamedQuery` on `OGuildSetting` can be removed. # Testing ### Validation Approach Run the existing suites unchanged with `./mvnw test`: `AbstractRepositoryTest`, `*RepositoryTest`, `GuildServiceTest` and `BlacklistServiceTest`. They cover the public repository behaviour. ### Key Scenarios - `upsertOrDelete`: insert, update and reset-to-default delete. - `TaskRepository.upsert` followed by `deleteByTaskId`. - `enableWord` returns `CREATED`, then `UNCHANGED`, and `ENABLED` after the word has been disabled. - `findOrCreate` is idempotent. ### Edge Cases - A failing write rolls back. - An HQL typo breaks `mvn compile`, which confirms the processor is active. ### Test Changes - Rename `AbstractRepositoryTest` to `TransactionRunnerTest`, or rework it so it no longer depends on the removed base class. # Delivery Steps ### ✓ Step 1: Add Jakarta Data and hibernate-processor to the build The project compiles with the annotation processor generating the metamodel and repository classes. - Add the `jakarta.data-api` dependency to `pom.xml`. - Configure `maven-compiler-plugin` with `annotationProcessorPaths` pointing to `hibernate-processor` 7.1.9.Final. - Add `db/TransactionRunner` with stateless read and transaction helpers. - Check that `./mvnw compile` produces classes like `OGuild_`. ### ✓ Step 2: Migrate Guild and Task repositories `GuildRepository` and `TaskRepository` delegate to the generated `GuildQueries_` and `TaskQueries_`. - Create `db/data/GuildQueries` and `db/data/TaskQueries`. - Rewrite `findOrCreate`, `upsert`, `findByTaskId` and `deleteByTaskId` using explicit `@Insert`/`@Update`. - Run `GuildServiceTest` and `TaskRepositoryTest`. ### ✓ Step 3: Migrate GuildSetting, Blacklist and Message repositories All remaining repositories use the generated queries, with transactional logic kept in the facades. - Create `GuildSettingQueries`, `BlacklistQueries` and `MessageQueries`. - Port `upsertOrDelete`, `enableWord` and `deleteWord` to explicit writes. - Remove the `settings_findByName` named query. - Run the matching repository and service tests. ### ✓ Step 4: Remove AbstractRepository and finish `AbstractRepository` is gone and the full test suite passes. - Delete `db/AbstractRepository.java` and move the generic `findAll`/`findById`/`save`/`deleteById` onto `CrudRepository`. - Rework `AbstractRepositoryTest` into `TransactionRunnerTest`. - Run `./mvnw verify`, including checkstyle and the shaded jar.