Files
netneutrality/.junie/plans/jakarta-data-repositories.md
T
anthorob bceab4b2b9 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.
2026-09-25 12:48:48 -04:00

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-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:
@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; });
  • 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.