Refactor repository layer using Jakarta Data

Replaced custom repository logic with Jakarta Data interfaces to reduce boilerplate and enhance compile-time query validation. Removed `AbstractRepository` and introduced `TransactionRunner` for consistent transactional support. Updated all repository facades to delegate query operations to generated interfaces while retaining the same API, ensuring backward compatibility. Added dependencies for `jakarta.data-api` and Hibernate's `hibernate-processor` for annotation-based query generation. Verified functional parity and improved testability across the repository layer.
This commit is contained in:
anthorob committed 2026-09-25 12:48:48 -04:00
1 parent 677b3f8013
commit bceab4b2b9
16 files changed
+604 -230

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
---
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<T,ID>` 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> R inTx(Function<StatelessSession,R>)` 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<OTask,Integer> {
@Find Optional<OTask> byTaskId(String taskId);
@Query("delete from OTask where taskId = :taskId") int deleteByTaskId(String taskId);
}
@Repository
public interface GuildSettingQueries extends CrudRepository<OGuildSetting,Integer> {
@Query("where guild.guildId = :guildId") List<OGuildSetting> byGuild(String guildId);
@Query("where guild.guildId = :id and name = :name") List<OGuildSetting> 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.