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.
117 lines
6.9 KiB
Markdown
117 lines
6.9 KiB
Markdown
---
|
|
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. |