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.
6.9 KiB
sessionId
| 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-apiplushibernate-processoras an annotation processor. - Generated query interfaces for
OGuild,OGuildSetting,OTask,OBlacklistandOLogMessage. - The existing repository classes become thin transactional facades over the generated code.
AbstractRepositoryis 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,saveanddeleteById. - 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 repositoryread/write/writeVoidhelpers over a statefulSession, usingSessionFactory.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) asnew XRepository(sessions). Tests construct them the same way.
Key Decisions
- Generated interfaces run on a
StatelessSession. This is the Hibernate Data default: the generated*_class takes aStatelessSessionin its constructor. There's no container, so transactions stay our responsibility. - The facade pattern keeps the public API. Classes like
GuildSettingRepositorystay as the public API. Each call opens a stateless transaction and creates the generated DAO.App, the services and the tests don't change. - 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@Deleteor a@Query("delete ...").session.getReference(OGuild…)becomes a detachedOGuildwith only its id set, which is enough for the foreign key on insert.
Proposed Changes
db/TransactionRunner.java(new) replacesAbstractRepository. It exposes<R> R inTx(Function<StatelessSession,R>)usingSessionFactory.fromStatelessTransaction, andread(...)usingfromStatelessSession.db/data/*Queries.java(new) holds the@Repositoryinterfaces, for example:
@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:
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; });
BlacklistQueriescoversbyGuild(ordered by id),enabledWords(aselect o.wordprojection) andfindWord(case-insensitiveupper()).MessageQueriesholds the same kind of migration forMessageRepository.pom.xmlgets:jakarta.data:jakarta.data-api:1.0.1maven-compiler-pluginwithannotationProcessorPathspointing toorg.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:mergebecomesupsert, available onCrudRepository.- Named query
settings_findByName: it's replaced by@Query. The@NamedQueryonOGuildSettingcan 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.upsertfollowed bydeleteByTaskId.enableWordreturnsCREATED, thenUNCHANGED, andENABLEDafter the word has been disabled.findOrCreateis idempotent.
Edge Cases
- A failing write rolls back.
- An HQL typo breaks
mvn compile, which confirms the processor is active.
Test Changes
- Rename
AbstractRepositoryTesttoTransactionRunnerTest, 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-apidependency topom.xml. - Configure
maven-compiler-pluginwithannotationProcessorPathspointing tohibernate-processor7.1.9.Final. - Add
db/TransactionRunnerwith stateless read and transaction helpers. - Check that
./mvnw compileproduces classes likeOGuild_.
✓ Step 2: Migrate Guild and Task repositories
GuildRepository and TaskRepository delegate to the generated GuildQueries_ and TaskQueries_.
- Create
db/data/GuildQueriesanddb/data/TaskQueries. - Rewrite
findOrCreate,upsert,findByTaskIdanddeleteByTaskIdusing explicit@Insert/@Update. - Run
GuildServiceTestandTaskRepositoryTest.
✓ Step 3: Migrate GuildSetting, Blacklist and Message repositories
All remaining repositories use the generated queries, with transactional logic kept in the facades.
- Create
GuildSettingQueries,BlacklistQueriesandMessageQueries. - Port
upsertOrDelete,enableWordanddeleteWordto explicit writes. - Remove the
settings_findByNamenamed 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.javaand move the genericfindAll/findById/save/deleteByIdontoCrudRepository. - Rework
AbstractRepositoryTestintoTransactionRunnerTest. - Run
./mvnw verify, including checkstyle and the shaded jar.