feat: introduce WebSocket support with new endpoints and transaction propagation enhancements

This commit is contained in:
Relism
2026-05-11 16:14:26 +02:00
parent a4a16bdb00
commit ccc5550598
55 changed files with 2705 additions and 752 deletions
@@ -0,0 +1,81 @@
# flash-ext-data-core
Core comune per il layer dati di Flash.
## Scopo
Questo modulo definisce il contratto transazionale condiviso tra le implementazioni backend.
Non parla con Hibernate o JDBC direttamente: espone solo astrazioni e un runtime minimale.
## Componenti
- `TxDefinition`: metadata immutabile della transazione.
- `TxStatus`: stato runtime restituito dal manager.
- `TxManager`: contratto per `begin`, `commit`, `rollback`.
- `Tx`: orchestration runtime e stack transazionale per thread.
- `ResourceRegistry`: storage thread-local di risorse e synchronizations.
- `Repository<T, ID>`: base repository auto-transazionale.
- `Spec<T>`: predicato componibile.
- `Query<T>`: oggetto query con spec, sort e paging.
- `SpecBuilder<T>`: DSL fluente per costruire spec tipizzate.
- `RepositorySupport<T, ID>`: helper interno condiviso.
- `TransactionPropagation`: semantica di propagazione.
- `TransactionIsolation`: livello di isolamento.
- `TxSynchronization`: hook lifecycle.
## Modello di esecuzione
Il flusso è:
1. `Tx.call(definition, work)` chiama `TxManager.begin(definition)`.
2. Il `TxManager` crea un `TxStatus` backend-specific.
3. Lo status viene pushato nello stack thread-local.
4. Il lavoro usa `Tx.resource(Class)` per ottenere la risorsa corrente.
5. A fine lavoro `Tx` decide tra `commit` e `rollback`.
6. Lo stack viene poppato e il thread-local viene pulito se vuoto.
## Propagation supportata
- `REQUIRED`: usa la tx attiva oppure ne apre una nuova.
- `REQUIRES_NEW`: sospende la tx corrente e apre una nuova tx.
- `SUPPORTS`: se esiste una tx attiva si aggancia, altrimenti esegue senza tx.
- `NOT_SUPPORTED`: sospende la tx corrente ed esegue senza tx.
- `MANDATORY`: richiede una tx attiva.
## Uso di `Repository`
`Repository` è la base comune per le repository concrete.
Ogni operazione pubblica usa internamente una tx `REQUIRED` o `REQUIRED` read-only.
Le sottoclassi implementano i metodi `doXxx(...)` del nuovo modello:
- `doFind(Query<T>)`
- `doFindOne(Spec<T>)`
- `doFindPage(Query<T>)`
- `doDeleteAll(Spec<T>)`
- `doUpdateAll(Spec<T>, T)`
I vecchi overload di `findAll(...)` e `findPage(...)` sono stati ridotti a una combinazione di `Query<T>` e `Spec<T>`.
```java
public abstract class Repository<T, ID> {
protected Repository(Tx tx) { ... }
protected final <R> R tx(Tx.TxCallable<R> work) { ... }
}
```
## Composizione con Flash
`DataExtension` registra:
- `Tx` nel `FlashContext`
- `TxManager` nel `FlashContext`
- un annotation processor per `@Transactional`
Questo rende il layer dati componibile con il sistema di extension di Flash senza stato globale.
## Note implementative
- Lo stack transazionale è thread-local e viene ripulito quando torna vuoto.
- Le risorse backend sono sospese e ripristinate per `REQUIRES_NEW` e `NOT_SUPPORTED`.
- `TxSynchronization` è il punto di aggancio per hook di commit/rollback/completion.
@@ -7,7 +7,6 @@ import dev.relism.flash.ext.data.core.TransactionPropagation;
import dev.relism.flash.extension.ExtensionPhase;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashExtension;
import dev.relism.flash.extension.FlashRegistrar;
import dev.relism.flash.routing.Middleware;
import jakarta.transaction.Transactional;
@@ -16,14 +15,16 @@ import java.util.Objects;
public final class DataExtension implements FlashExtension {
private final TxManager txManager;
private final Tx tx;
public DataExtension(TxManager txManager) {
this.txManager = Objects.requireNonNull(txManager);
this.tx = new Tx(txManager);
}
@Override
public void provide(FlashContext ctx) {
Tx.init(txManager);
ctx.provide(Tx.class, tx);
ctx.provide(TxManager.class, txManager);
ctx.addAnnotationProcessor(handlerClass -> {
Transactional ann = handlerClass.getAnnotation(Transactional.class);
@@ -33,7 +34,7 @@ public final class DataExtension implements FlashExtension {
TxDefinition definition = TxDefinition.DEFAULTS
.withPropagation(mapTxType(ann.value()));
Middleware middleware = next -> (req, res) -> {
return Tx.call(definition, () -> next.handle(req, res));
return tx.call(definition, () -> next.handle(req, res));
};
return List.of(middleware);
});
@@ -46,8 +47,9 @@ public final class DataExtension implements FlashExtension {
private TransactionPropagation mapTxType(Transactional.TxType txType) {
return switch (txType) {
case REQUIRED, SUPPORTS -> TransactionPropagation.REQUIRED;
case REQUIRED -> TransactionPropagation.REQUIRED;
case REQUIRES_NEW -> TransactionPropagation.REQUIRES_NEW;
case SUPPORTS -> TransactionPropagation.SUPPORTS;
case MANDATORY -> TransactionPropagation.MANDATORY;
case NOT_SUPPORTED, NEVER -> TransactionPropagation.NOT_SUPPORTED;
};
@@ -0,0 +1,28 @@
package dev.relism.flash.ext.data.core;
public record Query<T>(Spec<T> spec, Sort sort, Integer page, Integer size) {
public Query {
spec = spec == null ? Spec.all() : spec;
sort = sort == null ? Sort.unsorted() : sort;
}
public static <T> Query<T> all() {
return new Query<>(Spec.all(), Sort.unsorted(), null, null);
}
public Query<T> where(Spec<T> spec) {
return new Query<>(spec, sort, page, size);
}
public Query<T> orderBy(Sort sort) {
return new Query<>(spec, sort, page, size);
}
public Query<T> page(int page, int size) {
return new Query<>(spec, sort, page, size);
}
public boolean isPaged() {
return page != null && size != null;
}
}
@@ -4,109 +4,118 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Base repository. Subclasses only extend this — never HibernateRepository
* or JdbcRepository directly. The concrete backing is transparent.
*
* Every method auto-wraps in REQUIRED transaction — safe to call with or
* without an active transaction on the thread.
*/
public abstract class Repository<T, ID> {
public abstract class Repository<T, ID> extends RepositorySupport<T, ID> {
private final TxDefinition required = TxDefinition.DEFAULTS
.withPropagation(TransactionPropagation.REQUIRED);
// ── CRUD ──────────────────────────────────────────────────────────────────
protected Repository(Tx tx) {
super(tx);
}
public Optional<T> findById(ID id) {
return tx(() -> doFindById(id));
}
public List<T> findAll() {
return tx(this::doFindAll);
}
public List<T> findAll(int page, int size) {
return tx(() -> doFindAll(page, size));
}
public List<T> findAll(Sort sort) {
return tx(() -> doFindAll(sort));
}
public List<T> findAll(int page, int size, Sort sort) {
return tx(() -> doFindAll(page, size, sort));
}
public Page<T> findPage(int page, int size) {
return tx(() -> doFindPage(page, size));
}
public Page<T> findPage(int page, int size, Sort sort) {
return tx(() -> doFindPage(page, size, sort));
}
public T save(T entity) {
return tx(() -> doSave(entity));
}
public List<T> saveAll(Iterable<T> entities) {
return tx(() -> {
List<T> saved = new ArrayList<>();
for (T e : entities) saved.add(doSave(e));
return saved;
});
}
public T update(T entity) {
return tx(() -> doUpdate(entity));
}
public void delete(T entity) {
tx(() -> { doDelete(entity); return null; });
}
public void deleteById(ID id) {
tx(() -> { doDeleteById(id); return null; });
}
public void deleteAll(Iterable<T> entities) {
tx(() -> { entities.forEach(this::doDelete); return null; });
return roQuery(() -> doFindById(id));
}
public boolean existsById(ID id) {
return tx(() -> doExistsById(id));
return roQuery(() -> doExistsById(id));
}
public long count() {
return tx(this::doCount);
return roQuery(this::doCount);
}
// ── Auto-wrap helper ──────────────────────────────────────────────────────
/**
* Ensures the work runs inside a transaction.
* If one is already active (caller annotated @Transactional or inside Tx.run)
* it joins it — no new connection opened.
* If none is active it opens one, commits, and closes it transparently.
*/
protected final <R> R tx(Tx.TxCallable<R> work) {
return Tx.call(required, work);
public List<T> findAll() {
return findAll(Query.all());
}
// ── Abstract — implemented by HibernateRepository / JdbcRepository ────────
public List<T> findAll(Spec<T> spec) {
return findAll(Query.<T>all().where(spec));
}
public List<T> findAll(Query<T> query) {
return roQuery(() -> doFind(query));
}
public Page<T> findPage(Query<T> query) {
return roQuery(() -> doFindPage(query));
}
public Optional<T> findOne(Spec<T> spec) {
return roQuery(() -> doFindOne(spec));
}
public T save(T entity) {
return rwQuery(() -> doSave(entity));
}
public T update(T entity) {
return rwQuery(() -> doUpdate(entity));
}
public List<T> saveAll(Iterable<T> entities) {
return rwQuery(() -> doSaveAll(entities));
}
public void delete(T entity) {
rwQuery(() -> {
doDelete(entity);
return null;
});
}
public void deleteById(ID id) {
rwQuery(() -> {
doDeleteById(id);
return null;
});
}
public int deleteAll(Spec<T> spec) {
return rwQuery(() -> doDeleteAll(spec));
}
public int updateAll(Spec<T> spec, T patch) {
return rwQuery(() -> doUpdateAll(spec, patch));
}
public List<T> findAll(int page, int size) {
return findAll(Query.<T>all().page(page, size));
}
public List<T> findAll(Sort sort) {
return findAll(Query.<T>all().orderBy(sort));
}
public List<T> findAll(int page, int size, Sort sort) {
return findAll(Query.<T>all().orderBy(sort).page(page, size));
}
public Page<T> findPage(int page, int size) {
return findPage(Query.<T>all().page(page, size));
}
public Page<T> findPage(int page, int size, Sort sort) {
return findPage(Query.<T>all().orderBy(sort).page(page, size));
}
public void deleteAll(Iterable<T> entities) {
rwQuery(() -> {
for (T entity : entities) {
doDelete(entity);
}
return null;
});
}
protected abstract Optional<T> doFindById(ID id);
protected abstract List<T> doFindAll();
protected abstract List<T> doFindAll(int page, int size);
protected abstract List<T> doFindAll(Sort sort);
protected abstract List<T> doFindAll(int page, int size, Sort sort);
protected abstract Page<T> doFindPage(int page, int size);
protected abstract Page<T> doFindPage(int page, int size, Sort sort);
protected abstract T doSave(T entity);
protected abstract T doUpdate(T entity);
protected abstract void doDelete(T entity);
protected abstract void doDeleteById(ID id);
protected abstract boolean doExistsById(ID id);
protected abstract long doCount();
}
protected abstract List<T> doFind(Query<T> query);
protected abstract Optional<T> doFindOne(Spec<T> spec);
protected abstract Page<T> doFindPage(Query<T> query);
protected abstract boolean doExistsById(ID id);
protected abstract long doCount();
protected abstract T doSave(T entity);
protected abstract List<T> doSaveAll(Iterable<T> entities);
protected abstract T doUpdate(T entity);
protected abstract void doDelete(T entity);
protected abstract void doDeleteById(ID id);
protected abstract int doDeleteAll(Spec<T> spec);
protected abstract int doUpdateAll(Spec<T> spec, T patch);
}
@@ -0,0 +1,24 @@
package dev.relism.flash.ext.data.core;
public abstract class RepositorySupport<T, ID> {
private final Tx tx;
private final TxDefinition rw = TxDefinition.DEFAULTS
.withPropagation(TransactionPropagation.REQUIRED);
private final TxDefinition ro = rw.asReadOnly();
protected RepositorySupport(Tx tx) {
this.tx = tx;
}
protected final Tx tx() {
return tx;
}
protected final <R> R roQuery(Tx.TxCallable<R> work) {
return tx.call(ro, work);
}
protected final <R> R rwQuery(Tx.TxCallable<R> work) {
return tx.call(rw, work);
}
}
@@ -31,6 +31,11 @@ public final class ResourceRegistry {
SYNCHRONIZATIONS.get().clear();
}
public static void cleanup() {
RESOURCES.remove();
SYNCHRONIZATIONS.remove();
}
public static <R> R get(TxResourceKey key, Class<R> type) {
Object value = RESOURCES.get().get(key);
if (value == null) {
@@ -7,6 +7,10 @@ public record Sort(List<Column> columns) {
public record Column(String column, boolean asc) {}
public static Sort unsorted() { return new Sort(List.of()); }
public boolean isSorted() { return !columns.isEmpty(); }
public static Sort by(String column) { return new Sort(List.of(new Column(column, true))); }
public static Sort desc(String column) { return new Sort(List.of(new Column(column, false))); }
public static Sort by(String col, boolean asc){ return new Sort(List.of(new Column(col, asc))); }
@@ -19,4 +23,4 @@ public record Sort(List<Column> columns) {
next.add(new Column(column, asc));
return new Sort(next);
}
}
}
@@ -0,0 +1,26 @@
package dev.relism.flash.ext.data.core;
@FunctionalInterface
public interface Spec<T> {
String toFragment(SpecContext ctx);
default Spec<T> and(Spec<T> other) {
return ctx -> "(" + this.toFragment(ctx) + " AND " + other.toFragment(ctx) + ")";
}
default Spec<T> or(Spec<T> other) {
return ctx -> "(" + this.toFragment(ctx) + " OR " + other.toFragment(ctx) + ")";
}
default Spec<T> not() {
return ctx -> "NOT (" + this.toFragment(ctx) + ")";
}
static <T> Spec<T> all() {
return ctx -> "1=1";
}
static <T> Spec<T> none() {
return ctx -> "1=0";
}
}
@@ -0,0 +1,37 @@
package dev.relism.flash.ext.data.core;
import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;
public final class SpecBuilder<T> {
private SpecBuilder() {}
public static <T, V> FieldSpec<T, V> field(String column) {
return new FieldSpec<>(column);
}
public static final class FieldSpec<T, V> {
private final String column;
private FieldSpec(String column) {
this.column = Objects.requireNonNull(column);
}
public Spec<T> eq(V value) { return ctx -> column + " = " + ctx.bind(value); }
public Spec<T> neq(V value) { return ctx -> column + " != " + ctx.bind(value); }
public Spec<T> like(String pattern) { return ctx -> column + " like " + ctx.bind(pattern); }
public Spec<T> isNull() { return ctx -> column + " is null"; }
public Spec<T> isNotNull() { return ctx -> column + " is not null"; }
public Spec<T> in(Collection<V> values) {
return ctx -> column + " in (" + values.stream().map(ctx::bind).collect(Collectors.joining(", ")) + ")";
}
public <C extends Comparable<C>> Spec<T> gt(C value) { return ctx -> column + " > " + ctx.bind(value); }
public <C extends Comparable<C>> Spec<T> lt(C value) { return ctx -> column + " < " + ctx.bind(value); }
public <C extends Comparable<C>> Spec<T> between(C lo, C hi) {
return ctx -> column + " between " + ctx.bind(lo) + " and " + ctx.bind(hi);
}
}
}
@@ -0,0 +1,5 @@
package dev.relism.flash.ext.data.core;
public interface SpecContext {
String bind(Object value);
}
@@ -3,6 +3,7 @@ package dev.relism.flash.ext.data.core;
public enum TransactionPropagation {
REQUIRED,
REQUIRES_NEW,
SUPPORTS,
NOT_SUPPORTED,
MANDATORY
}
@@ -2,83 +2,78 @@ package dev.relism.flash.ext.data.core;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
public final class Tx {
private static final ThreadLocal<Deque<TxStatus>> STATUS_STACK =
ThreadLocal.withInitial(ArrayDeque::new);
private static volatile TxManager manager;
private final TxManager manager;
private Tx() {}
public static void init(TxManager txManager) {
if (manager != null) {
throw new IllegalStateException("TxManager already initialized");
}
manager = txManager;
public Tx(TxManager txManager) {
this.manager = Objects.requireNonNull(txManager);
}
public static void run(TxRunnable work) {
public void run(TxRunnable work) {
run(TxDefinition.DEFAULTS, work);
}
public static void run(TxDefinition definition, TxRunnable work) {
public void run(TxDefinition definition, TxRunnable work) {
call(definition, () -> {
work.run();
return null;
});
}
public static <T> T call(TxCallable<T> work) {
public <T> T call(TxCallable<T> work) {
return call(TxDefinition.DEFAULTS, work);
}
public static <T> T call(TxDefinition definition, TxCallable<T> work) {
TxStatus status = manager().begin(definition);
public <T> T call(TxDefinition definition, TxCallable<T> work) {
TxStatus status = manager.begin(definition);
pushStatus(status);
try {
T result = work.call();
if (status.isRollbackOnly()) {
manager().rollback(status);
manager.rollback(status);
} else {
manager().commit(status);
manager.commit(status);
}
return result;
} catch (Exception e) {
manager().rollback(status);
silentRollback(status);
throw (e instanceof TxException txException) ? txException : new TxException(e);
} catch (Throwable t) {
silentRollback(status);
throw sneakyThrow(t);
} finally {
popStatus();
if (STATUS_STACK.get().isEmpty()) {
STATUS_STACK.remove();
}
}
}
public static boolean isActive() {
public boolean isActive() {
return !STATUS_STACK.get().isEmpty();
}
public static void setRollbackOnly() {
public void setRollbackOnly() {
currentStatus().markRollbackOnly();
}
public static <R> R resource(Class<R> type) {
public <R> R resource(Class<R> type) {
return currentStatus().resource(type);
}
public static TxDefinition requiresNew() {
public TxDefinition requiresNew() {
return TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW);
}
public static TxDefinition readOnly() {
public TxDefinition readOnly() {
return TxDefinition.DEFAULTS.asReadOnly();
}
private static TxManager manager() {
if (manager == null) {
throw new IllegalStateException("No TxManager installed");
}
return manager;
}
private static TxStatus currentStatus() {
private TxStatus currentStatus() {
TxStatus status = STATUS_STACK.get().peek();
if (status == null) {
throw new IllegalStateException("No active transaction");
@@ -86,17 +81,29 @@ public final class Tx {
return status;
}
private static void pushStatus(TxStatus status) {
private void pushStatus(TxStatus status) {
STATUS_STACK.get().push(status);
}
private static void popStatus() {
private void popStatus() {
Deque<TxStatus> stack = STATUS_STACK.get();
if (!stack.isEmpty()) {
stack.pop();
}
}
private void silentRollback(TxStatus status) {
try {
manager.rollback(status);
} catch (Exception ignored) {
}
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> RuntimeException sneakyThrow(Throwable t) throws E {
throw (E) t;
}
@FunctionalInterface
public interface TxRunnable {
void run();
@@ -0,0 +1,92 @@
# flash-ext-data-hibernate
Backend Hibernate per `flash-ext-data-core`.
## Scopo
Questo modulo implementa `TxManager` sopra `SessionFactory` e fornisce una base repository Hibernate-centric.
## Come si usa
### 1. Creare il manager
```java
SessionFactory sessionFactory = ...;
HibernateTxManager txManager = new HibernateTxManager(sessionFactory);
DataExtension extension = new DataExtension(txManager);
```
### 2. Installare lestensione in Flash
Lestensione registra `Tx` e `TxManager` nel `FlashContext`.
Le handler class-based annotate con `@Transactional` vengono wrappate automaticamente.
### 3. Definire una repository
```java
public final class UserRepository extends HibernateRepository<User, Long> {
public UserRepository(Tx tx) {
super(tx, User.class);
}
}
```
Con il nuovo modello query/spec puoi esporre campi riusabili come costanti:
```java
public final class UserRepository extends HibernateRepository<User, Long> {
public static final SpecBuilder.FieldSpec<User, String> EMAIL = SpecBuilder.field("u.email");
public static final SpecBuilder.FieldSpec<User, Boolean> ACTIVE = SpecBuilder.field("u.active");
public UserRepository(Tx tx) {
super(tx, User.class);
}
public Optional<User> findByEmail(String email) {
return findOne(EMAIL.eq(email));
}
}
```
Le query domain-specific possono usare gli helper della base class:
```java
public List<User> findByEmailDomain(String domain) {
return findMany("from User u where u.email like :email", q ->
q.setParameter("email", "%@" + domain)
);
}
```
## Come funziona sotto
- La tx corrente è rappresentata da `HibernateTxStatus`.
- La risorsa esposta al core è una `Session`.
- `Tx.resource(Session.class)` recupera la `Session` dal contesto corrente.
- `REQUIRES_NEW` sospende lo status attivo e apre una nuova `Session`.
- `NOT_SUPPORTED` sospende la tx attiva e continua senza sessione bindata.
## Repository base
`HibernateRepository` fornisce:
- `findById`, `findAll`, `findPage`, `findOne`
- `save`, `update`, `delete`, `saveAll`
- bulk `deleteAll(Spec<T>)` e `updateAll(Spec<T>, T)`
- helper HQL: `hql(...)`, `hqlMutate(...)`
Le classi concrete devono solo implementare query di dominio, non il plumbing transazionale.
## Semantica transazionale
- `REQUIRED`: join o apertura nuova tx.
- `REQUIRES_NEW`: sospensione del contesto corrente.
- `SUPPORTS`: join se c’è tx, altrimenti no-op.
- `NOT_SUPPORTED`: sospende e prosegue senza tx.
- `MANDATORY`: fallisce se non c’è tx.
## Note
- `Session` viene chiusa a fine tx nuova.
- Le synchronizations vengono eseguite al commit/rollback.
- Il backend è pensato per essere usato tramite la base class, non direttamente.
@@ -1,79 +1,74 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.*;
import jakarta.persistence.TypedQuery;
import org.hibernate.Session;
import org.hibernate.query.MutationQuery;
import jakarta.persistence.TypedQuery;
import java.io.Serializable;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Hibernate-backed repository base.
* Never extend this directly — extend {@link Repository} from the core.
* This class is instantiated internally by flash-ext-data-hibernate.
*/
public abstract class HibernateRepository<T, ID extends Serializable>
extends Repository<T, ID> {
public abstract class HibernateRepository<T, ID extends Serializable> extends Repository<T, ID> {
private final Class<T> type;
protected HibernateRepository(Class<T> type) {
protected HibernateRepository(Tx tx, Class<T> type) {
super(tx);
this.type = type;
}
// ── Session — always safe, tx() wrapper guarantees active transaction ─────
protected Session session() {
return Tx.resource(Session.class);
return tx().resource(Session.class);
}
// ── Repository abstract impl ──────────────────────────────────────────────
@Override
protected Optional<T> doFindById(ID id) {
return Optional.ofNullable(session().get(type, id));
}
@Override
protected List<T> doFindAll() {
return hql("from " + type.getSimpleName()).getResultList();
protected List<T> doFind(Query<T> query) {
HibernateSpecContext ctx = new HibernateSpecContext();
String where = query.spec() != null ? " where " + query.spec().toFragment(ctx) : "";
String order = query.sort() != null && query.sort().isSorted() ? " order by " + orderClause(query.sort()) : "";
TypedQuery<T> q = session().createQuery("from " + type.getSimpleName() + where + order, type);
ctx.applyParameters(q);
if (query.isPaged()) {
q.setFirstResult(query.page() * query.size());
q.setMaxResults(query.size());
}
return q.getResultList();
}
@Override
protected List<T> doFindAll(int page, int size) {
return hql("from " + type.getSimpleName())
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
protected Optional<T> doFindOne(Spec<T> spec) {
return doFind(Query.<T>all().where(spec).page(0, 1)).stream().findFirst();
}
@Override
protected List<T> doFindAll(Sort sort) {
return hql("from " + type.getSimpleName() + orderClause(sort))
.getResultList();
protected Page<T> doFindPage(Query<T> query) {
if (!query.isPaged()) {
throw new IllegalArgumentException("Paged query requires page and size");
}
long total = countWhere(query.spec());
List<T> content = doFind(query);
return new Page<>(content, query.page(), query.size(), total);
}
@Override
protected List<T> doFindAll(int page, int size, Sort sort) {
return hql("from " + type.getSimpleName() + orderClause(sort))
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
protected boolean doExistsById(ID id) {
return doFindById(id).isPresent();
}
@Override
protected Page<T> doFindPage(int page, int size) {
long total = doCount();
return new Page<>(doFindAll(page, size), page, size, total);
}
@Override
protected Page<T> doFindPage(int page, int size, Sort sort) {
long total = doCount();
return new Page<>(doFindAll(page, size, sort), page, size, total);
protected long doCount() {
return countWhere(Spec.all());
}
@Override
@@ -82,6 +77,22 @@ public abstract class HibernateRepository<T, ID extends Serializable>
return entity;
}
@Override
protected List<T> doSaveAll(Iterable<T> entities) {
List<T> saved = new ArrayList<>();
Session s = session();
int i = 0;
for (T entity : entities) {
s.persist(entity);
saved.add(entity);
if (++i % 50 == 0) {
s.flush();
s.clear();
}
}
return saved;
}
@Override
protected T doUpdate(T entity) {
return session().merge(entity);
@@ -99,66 +110,37 @@ public abstract class HibernateRepository<T, ID extends Serializable>
}
@Override
protected boolean doExistsById(ID id) {
return doFindById(id).isPresent();
protected int doDeleteAll(Spec<T> spec) {
HibernateSpecContext ctx = new HibernateSpecContext();
String where = " where " + spec.toFragment(ctx);
MutationQuery q = session().createMutationQuery("delete from " + type.getSimpleName() + where);
ctx.applyParameters(q);
return q.executeUpdate();
}
@Override
protected long doCount() {
return session()
.createQuery("select count(*) from " + type.getSimpleName(), Long.class)
.uniqueResultOptional()
.orElse(0L);
protected int doUpdateAll(Spec<T> spec, T patch) {
throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
}
// ── Query helpers — usabili nelle sottoclassi domain ─────────────────────
protected TypedQuery<T> hql(String hql) {
return session().createQuery(hql, type);
}
protected <R> TypedQuery<R> hql(String hql, Class<R> resultType) {
return session().createQuery(hql, resultType);
}
protected Optional<T> findOne(String hql, Consumer<TypedQuery<T>> params) {
TypedQuery<T> q = hql(hql);
params.accept(q);
return q.getResultStream().findFirst();
}
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params) {
return tx(() -> {
TypedQuery<T> q = hql(hql);
protected List<T> hql(String hql, Consumer<TypedQuery<T>> params) {
return roQuery(() -> {
TypedQuery<T> q = session().createQuery(hql, type);
params.accept(q);
return q.getResultList();
});
}
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params,
int page, int size) {
return tx(() -> {
TypedQuery<T> q = hql(hql);
protected <R> List<R> hql(String hql, Class<R> resultType, Consumer<TypedQuery<R>> params) {
return roQuery(() -> {
TypedQuery<R> q = session().createQuery(hql, resultType);
params.accept(q);
return q.setFirstResult(page * size).setMaxResults(size).getResultList();
return q.getResultList();
});
}
protected Page<T> findManyPaged(String hql, String countHql,
Consumer<TypedQuery<T>> params,
int page, int size) {
return tx(() -> {
long total = session()
.createQuery(countHql, Long.class)
.uniqueResultOptional()
.orElse(0L);
List<T> content = findMany(hql, params, page, size);
return new Page<>(content, page, size, total);
});
}
protected int execute(String hql, Consumer<MutationQuery> params) {
return tx(() -> {
protected int hqlMutate(String hql, Consumer<MutationQuery> params) {
return rwQuery(() -> {
MutationQuery q = session().createMutationQuery(hql);
params.accept(q);
return q.executeUpdate();
@@ -169,9 +151,17 @@ public abstract class HibernateRepository<T, ID extends Serializable>
return type;
}
private long countWhere(Spec<T> spec) {
HibernateSpecContext ctx = new HibernateSpecContext();
String where = spec != null ? " where " + spec.toFragment(ctx) : "";
TypedQuery<Long> q = session().createQuery("select count(*) from " + type.getSimpleName() + where, Long.class);
ctx.applyParameters(q);
return q.getResultStream().findFirst().orElse(0L);
}
private String orderClause(Sort sort) {
return " order by " + sort.columns().stream()
return sort.columns().stream()
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
.collect(Collectors.joining(", "));
}
}
}
@@ -0,0 +1,28 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.SpecContext;
import jakarta.persistence.TypedQuery;
import org.hibernate.query.MutationQuery;
import java.util.LinkedHashMap;
import java.util.Map;
final class HibernateSpecContext implements SpecContext {
private final Map<String, Object> params = new LinkedHashMap<>();
private int counter;
@Override
public String bind(Object value) {
String name = "p" + (++counter);
params.put(name, value);
return ":" + name;
}
void applyParameters(TypedQuery<?> query) {
params.forEach(query::setParameter);
}
void applyParameters(MutationQuery query) {
params.forEach(query::setParameter);
}
}
@@ -8,6 +8,7 @@ import java.util.Objects;
public class HibernateTxManager implements TxManager {
private static final TxResourceKey HIBERNATE_STATUS_KEY = TxResourceKey.of("hibernate.tx.status");
private static final TxResourceKey HIBERNATE_SUSPENDED_KEY = TxResourceKey.of("hibernate.tx.suspended");
private final SessionFactory sf;
@@ -22,35 +23,56 @@ public class HibernateTxManager implements TxManager {
? joinExisting(definition)
: beginNew(definition);
case REQUIRES_NEW -> beginNew(definition);
case SUPPORTS -> ResourceRegistry.isBound(HIBERNATE_STATUS_KEY)
? joinExisting(definition)
: noOp(definition);
case MANDATORY -> {
if (!ResourceRegistry.isBound(HIBERNATE_STATUS_KEY))
throw new IllegalStateException("MANDATORY: no active transaction");
yield joinExisting(definition);
}
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
case NOT_SUPPORTED -> {
HibernateTxStatus suspended = suspendIfNeeded();
yield noOp(definition, suspended);
}
};
}
private TxStatus beginNew(TxDefinition definition) {
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
}
return beginNew(definition, suspendIfNeeded());
}
private TxStatus beginNew(TxDefinition definition, HibernateTxStatus suspended) {
Session s = sf.openSession();
s.beginTransaction();
if (definition.readOnly()) s.setDefaultReadOnly(true);
if (definition.isolation() != TransactionIsolation.DEFAULT) {
s.doWork(connection -> connection.setTransactionIsolation(definition.isolation().level()));
boolean bound = false;
try {
s.beginTransaction();
if (definition.readOnly()) s.setDefaultReadOnly(true);
if (definition.isolation() != TransactionIsolation.DEFAULT) {
s.doWork(connection -> connection.setTransactionIsolation(definition.isolation().level()));
}
HibernateTxStatus status = new HibernateTxStatus(
s,
true,
definition.readOnly(),
suspended,
new HibernateTxStatus.RollbackMarker()
);
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, status);
bound = true;
return status;
} catch (RuntimeException e) {
silentClose(s);
throw e;
} catch (Exception e) {
silentClose(s);
throw new TxException(e);
} finally {
if (!bound && suspended != null) {
ResourceRegistry.unbind(HIBERNATE_SUSPENDED_KEY);
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
}
}
HibernateTxStatus status = new HibernateTxStatus(
s,
true,
definition.readOnly(),
suspended,
new HibernateTxStatus.RollbackMarker()
);
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, status);
return status;
}
private TxStatus joinExisting(TxDefinition definition) {
@@ -67,10 +89,29 @@ public class HibernateTxManager implements TxManager {
);
}
private TxStatus noOp(TxDefinition definition) {
return noOp(definition, null);
}
private TxStatus noOp(TxDefinition definition, HibernateTxStatus suspended) {
return new HibernateTxStatus(null, false, definition.readOnly(), suspended, new HibernateTxStatus.RollbackMarker());
}
private HibernateTxStatus suspendIfNeeded() {
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
ResourceRegistry.bind(HIBERNATE_SUSPENDED_KEY, suspended);
}
return suspended;
}
@Override
public void commit(TxStatus status) {
HibernateTxStatus s = (HibernateTxStatus) status;
if (!s.isNewTransaction()) {
resumeIfNeeded(s);
cleanupIfIdle();
return;
}
try {
@@ -83,6 +124,7 @@ public class HibernateTxManager implements TxManager {
}
} finally {
cleanupAndResume(s);
cleanupIfIdle();
}
}
@@ -91,6 +133,8 @@ public class HibernateTxManager implements TxManager {
HibernateTxStatus s = (HibernateTxStatus) status;
if (!s.isNewTransaction()) {
s.markRollbackOnly();
resumeIfNeeded(s);
cleanupIfIdle();
return;
}
try {
@@ -100,15 +144,40 @@ public class HibernateTxManager implements TxManager {
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
} finally {
cleanupAndResume(s);
cleanupIfIdle();
}
}
private void cleanupAndResume(HibernateTxStatus status) {
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
status.session().close();
silentClose(status.session());
resumeIfNeeded(status);
}
private void cleanupIfIdle() {
if (!ResourceRegistry.isBound(HIBERNATE_STATUS_KEY) && !ResourceRegistry.isBound(HIBERNATE_SUSPENDED_KEY)) {
ResourceRegistry.cleanup();
}
}
private void resumeIfNeeded(HibernateTxStatus status) {
HibernateTxStatus suspended = status.suspended();
if (suspended == null) {
suspended = ResourceRegistry.getOrNull(HIBERNATE_SUSPENDED_KEY, HibernateTxStatus.class);
}
if (suspended != null) {
ResourceRegistry.unbind(HIBERNATE_SUSPENDED_KEY);
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
}
}
private void silentClose(Session session) {
if (session == null) {
return;
}
try {
session.close();
} catch (Exception ignored) {
}
}
}
@@ -35,6 +35,9 @@ class HibernateTxStatus implements TxStatus {
@Override
public <R> R resource(Class<R> type) {
if (session == null) {
throw new IllegalStateException("No session bound to this transaction status");
}
return type.cast(session);
}
@@ -0,0 +1,96 @@
# flash-ext-data-jdbc
Backend JDBC per `flash-ext-data-core`.
## Scopo
Questo modulo implementa `TxManager` sopra `DataSource` e fornisce una base repository SQL raw.
## Come si usa
### 1. Creare il manager
```java
DataSource dataSource = ...;
JdbcTxManager txManager = new JdbcTxManager(dataSource);
DataExtension extension = new DataExtension(txManager);
```
### 2. Installare lestensione in Flash
Come per Hibernate, `DataExtension` registra `Tx` nel `FlashContext` e abilita `@Transactional` sugli handler class-based.
### 3. Definire una repository
```java
public final class UserRepository extends JdbcRepository<User, Long> {
public UserRepository(Tx tx) {
super(tx, "users", "id");
}
@Override
protected User mapRow(ResultSet rs) throws SQLException {
return new User(rs.getLong("id"), rs.getString("name"));
}
}
```
Anche qui puoi esporre `Spec` riusabili e comporre query dal service layer:
```java
public final class UserRepository extends JdbcRepository<User, Long> {
public static final SpecBuilder.FieldSpec<User, String> EMAIL = SpecBuilder.field("email");
public UserRepository(Tx tx) {
super(tx, "users", "id");
}
}
```
Per il salvataggio e lupdate devi fornire il binding esplicito:
```java
@Override
protected String insertSql() {
return "insert into users(name) values(?)";
}
@Override
protected void bindInsert(PreparedStatement ps, User entity) throws SQLException {
ps.setString(1, entity.name());
}
```
## Come funziona sotto
- La tx corrente espone una `Connection`.
- `Tx.resource(Connection.class)` recupera la connessione bindata al thread.
- `REQUIRES_NEW` sospende la connessione attiva e ne apre una nuova.
- `NOT_SUPPORTED` sospende il contesto e prosegue senza tx.
## Repository base
`JdbcRepository` fornisce:
- query `select` con `queryOne`, `queryMany`
- mutation con `mutate`
- persistenza con `doSave`, `doUpdate`
- paging con `doFindPage`
- bulk `deleteAll(Spec<T>)`
- helper raw `queryOne(...)`, `queryMany(...)`, `mutate(...)`
Le repository concrete devono solo tradurre tra `ResultSet` e dominio.
## Semantica transazionale
- `REQUIRED`: join o apertura nuova tx.
- `REQUIRES_NEW`: sospensione del contesto corrente.
- `SUPPORTS`: join se c’è tx, altrimenti no-op.
- `NOT_SUPPORTED`: sospende e prosegue senza tx.
- `MANDATORY`: fallisce se non c’è tx.
## Note
- La `Connection` viene chiusa a fine tx nuova.
- Le synchronizations vengono eseguite al commit/rollback.
- Se una repository usa `doDelete(T)`, il comportamento predefinito è non supportato: usare `deleteById` o override specifico.
@@ -3,87 +3,99 @@ package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.*;
import java.sql.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
private final String table;
private final String idColumn;
protected JdbcRepository(String table, String idColumn) {
this.table = table;
protected JdbcRepository(Tx tx, String table, String idColumn) {
super(tx);
this.table = table;
this.idColumn = idColumn;
}
protected Connection connection() {
return Tx.resource(Connection.class);
return tx().resource(Connection.class);
}
// ── Subclass contract ─────────────────────────────────────────────────────
protected abstract T mapRow(ResultSet rs) throws SQLException;
protected abstract void bindInsert(PreparedStatement ps, T entity) throws SQLException;
protected abstract void bindUpdate(PreparedStatement ps, T entity) throws SQLException;
protected abstract T mapRow(ResultSet rs) throws SQLException;
protected abstract void bindInsert(PreparedStatement ps, T entity) throws SQLException;
protected abstract void bindUpdate(PreparedStatement ps, T entity) throws SQLException;
protected abstract String insertSql();
protected abstract String updateSql();
// ── Repository abstract impl ──────────────────────────────────────────────
@Override
protected Optional<T> doFindById(ID id) {
return queryOne("select * from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id));
return queryOne("select * from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id));
}
@Override
protected List<T> doFindAll() {
return queryMany("select * from " + table, ps -> {});
}
protected List<T> doFind(Query<T> query) {
JdbcSpecContext ctx = new JdbcSpecContext();
String where = query.spec() != null ? " where " + query.spec().toFragment(ctx) : "";
String order = query.sort() != null && query.sort().isSorted() ? " order by " + orderClause(query.sort()) : "";
String paging = query.isPaged() ? " limit ? offset ?" : "";
@Override
protected List<T> doFindAll(int page, int size) {
return queryMany("select * from " + table + " limit ? offset ?", ps -> {
ps.setInt(1, size);
ps.setInt(2, page * size);
return queryMany("select * from " + table + where + order + paging, ps -> {
if (query.isPaged()) {
ctx.applyParameters(ps);
int base = ctx.size();
ps.setInt(base + 1, query.size());
ps.setInt(base + 2, query.page() * query.size());
return;
}
ctx.applyParameters(ps);
});
}
@Override
protected List<T> doFindAll(Sort sort) {
return queryMany("select * from " + table + orderClause(sort), ps -> {});
protected Optional<T> doFindOne(Spec<T> spec) {
return doFind(Query.<T>all().where(spec).page(0, 1)).stream().findFirst();
}
@Override
protected List<T> doFindAll(int page, int size, Sort sort) {
return queryMany("select * from " + table + orderClause(sort) + " limit ? offset ?",
ps -> {
ps.setInt(1, size);
ps.setInt(2, page * size);
});
protected Page<T> doFindPage(Query<T> query) {
if (!query.isPaged()) {
throw new IllegalArgumentException("Paged query requires page and size");
}
long total = countWhere(query.spec());
List<T> content = doFind(query);
return new Page<>(content, query.page(), query.size(), total);
}
@Override
protected Page<T> doFindPage(int page, int size) {
long total = doCount();
return new Page<>(doFindAll(page, size), page, size, total);
protected boolean doExistsById(ID id) {
return queryOne("select 1 from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id), rs -> rs.getInt(1)).isPresent();
}
@Override
protected Page<T> doFindPage(int page, int size, Sort sort) {
long total = doCount();
return new Page<>(doFindAll(page, size, sort), page, size, total);
protected long doCount() {
return queryOne("select count(*) from " + table, ps -> {}, rs -> rs.getLong(1)).orElse(0L);
}
@Override
protected T doSave(T entity) {
try (PreparedStatement ps = connection().prepareStatement(
insertSql(), Statement.RETURN_GENERATED_KEYS)) {
try (PreparedStatement ps = connection().prepareStatement(insertSql(), Statement.RETURN_GENERATED_KEYS)) {
bindInsert(ps, entity);
ps.executeUpdate();
applyGeneratedKey(ps, entity);
return entity;
} catch (SQLException e) { throw new TxException(e); }
} catch (SQLException e) {
throw new TxException(e);
}
}
@Override
protected List<T> doSaveAll(Iterable<T> entities) {
List<T> saved = new ArrayList<>();
for (T entity : entities) {
saved.add(doSave(entity));
}
return saved;
}
@Override
@@ -92,7 +104,9 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
bindUpdate(ps, entity);
ps.executeUpdate();
return entity;
} catch (SQLException e) { throw new TxException(e); }
} catch (SQLException e) {
throw new TxException(e);
}
}
@Override
@@ -102,38 +116,35 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
@Override
protected void doDeleteById(ID id) {
mutate("delete from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id));
mutate("delete from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id));
}
@Override
protected boolean doExistsById(ID id) {
return queryOne("select 1 from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id),
rs -> rs.getInt(1)).isPresent();
protected int doDeleteAll(Spec<T> spec) {
JdbcSpecContext ctx = new JdbcSpecContext();
String where = " where " + spec.toFragment(ctx);
return mutate("delete from " + table + where, ctx::applyParameters);
}
@Override
protected long doCount() {
return queryOne("select count(*) from " + table, ps -> {},
rs -> rs.getLong(1)).orElse(0L);
protected int doUpdateAll(Spec<T> spec, T patch) {
throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
}
// ── Query helpers ─────────────────────────────────────────────────────────
protected Optional<T> queryOne(String sql, SqlBinder params) {
List<T> r = queryMany(sql, params);
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
}
protected <R> Optional<R> queryOne(String sql, SqlBinder params,
SqlMapper<R> mapper) {
protected <R> Optional<R> queryOne(String sql, SqlBinder params, SqlMapper<R> mapper) {
try (PreparedStatement ps = connection().prepareStatement(sql)) {
params.bind(ps);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? Optional.of(mapper.map(rs)) : Optional.empty();
}
} catch (SQLException e) { throw new TxException(e); }
} catch (SQLException e) {
throw new TxException(e);
}
}
protected List<T> queryMany(String sql, SqlBinder params) {
@@ -144,26 +155,47 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
while (rs.next()) results.add(mapRow(rs));
return results;
}
} catch (SQLException e) { throw new TxException(e); }
} catch (SQLException e) {
throw new TxException(e);
}
}
protected int mutate(String sql, SqlBinder params) {
try (PreparedStatement ps = connection().prepareStatement(sql)) {
params.bind(ps);
return ps.executeUpdate();
} catch (SQLException e) { throw new TxException(e); }
} catch (SQLException e) {
throw new TxException(e);
}
}
protected void applyGeneratedKey(PreparedStatement ps, T entity) throws SQLException {
// override when entity has a generated PK
}
private String orderClause(Sort sort) {
return " order by " + sort.columns().stream()
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
.collect(Collectors.joining(", "));
protected Class<T> entityType() {
return null;
}
@FunctionalInterface public interface SqlBinder { void bind(PreparedStatement ps) throws SQLException; }
@FunctionalInterface public interface SqlMapper<R> { R map(ResultSet rs) throws SQLException; }
}
private long countWhere(Spec<T> spec) {
JdbcSpecContext ctx = new JdbcSpecContext();
String where = spec != null ? " where " + spec.toFragment(ctx) : "";
return queryOne("select count(*) from " + table + where, ctx::applyParameters, rs -> rs.getLong(1)).orElse(0L);
}
private String orderClause(Sort sort) {
return sort.columns().stream()
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
.collect(java.util.stream.Collectors.joining(", "));
}
@FunctionalInterface
public interface SqlBinder {
void bind(PreparedStatement ps) throws SQLException;
}
@FunctionalInterface
public interface SqlMapper<R> {
R map(ResultSet rs) throws SQLException;
}
}
@@ -0,0 +1,28 @@
package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.SpecContext;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
final class JdbcSpecContext implements SpecContext {
private final List<Object> params = new ArrayList<>();
@Override
public String bind(Object value) {
params.add(value);
return "?";
}
void applyParameters(PreparedStatement ps) throws SQLException {
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
}
int size() {
return params.size();
}
}
@@ -9,6 +9,7 @@ import java.util.Objects;
public class JdbcTxManager implements TxManager {
private static final TxResourceKey JDBC_STATUS_KEY = TxResourceKey.of("jdbc.tx.status");
private static final TxResourceKey JDBC_SUSPENDED_KEY = TxResourceKey.of("jdbc.tx.suspended");
private final DataSource ds;
@@ -23,22 +24,27 @@ public class JdbcTxManager implements TxManager {
? joinExisting(definition)
: beginNew(definition);
case REQUIRES_NEW -> beginNew(definition);
case SUPPORTS -> ResourceRegistry.isBound(JDBC_STATUS_KEY)
? joinExisting(definition)
: noOp(definition);
case MANDATORY -> {
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY))
throw new IllegalStateException("MANDATORY: no active transaction");
yield joinExisting(definition);
}
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
case NOT_SUPPORTED -> {
JdbcTxStatus suspended = suspendIfNeeded();
yield noOp(definition, suspended);
}
};
}
private TxStatus beginNew(TxDefinition definition) {
Connection conn = null;
JdbcTxStatus suspended = suspendIfNeeded();
boolean bound = false;
try {
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(JDBC_STATUS_KEY);
}
Connection conn = ds.getConnection();
conn = ds.getConnection();
conn.setAutoCommit(false);
if (definition.readOnly()) conn.setReadOnly(true);
if (definition.isolation() != TransactionIsolation.DEFAULT) {
@@ -52,9 +58,16 @@ public class JdbcTxManager implements TxManager {
new JdbcTxStatus.RollbackMarker()
);
ResourceRegistry.bind(JDBC_STATUS_KEY, status);
bound = true;
return status;
} catch (SQLException e) {
silentClose(conn);
throw new TxException(e);
} finally {
if (!bound && suspended != null) {
ResourceRegistry.unbind(JDBC_SUSPENDED_KEY);
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
}
}
}
@@ -72,10 +85,29 @@ public class JdbcTxManager implements TxManager {
);
}
private TxStatus noOp(TxDefinition definition) {
return noOp(definition, null);
}
private TxStatus noOp(TxDefinition definition, JdbcTxStatus suspended) {
return new JdbcTxStatus(null, false, definition.readOnly(), suspended, new JdbcTxStatus.RollbackMarker());
}
private JdbcTxStatus suspendIfNeeded() {
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(JDBC_STATUS_KEY);
ResourceRegistry.bind(JDBC_SUSPENDED_KEY, suspended);
}
return suspended;
}
@Override
public void commit(TxStatus status) {
JdbcTxStatus s = (JdbcTxStatus) status;
if (!s.isNewTransaction()) {
resumeIfNeeded(s);
cleanupIfIdle();
return;
}
try {
@@ -90,6 +122,7 @@ public class JdbcTxManager implements TxManager {
throw new TxException(e);
} finally {
cleanupAndResume(s);
cleanupIfIdle();
}
}
@@ -98,6 +131,8 @@ public class JdbcTxManager implements TxManager {
JdbcTxStatus s = (JdbcTxStatus) status;
if (!s.isNewTransaction()) {
s.markRollbackOnly();
resumeIfNeeded(s);
cleanupIfIdle();
return;
}
try {
@@ -107,18 +142,45 @@ public class JdbcTxManager implements TxManager {
throw new TxException(e);
} finally {
cleanupAndResume(s);
cleanupIfIdle();
}
}
private void cleanupAndResume(JdbcTxStatus status) {
ResourceRegistry.unbind(JDBC_STATUS_KEY);
try {
status.connection().close();
if (status.connection() != null) {
status.connection().close();
}
} catch (SQLException ignored) {
}
resumeIfNeeded(status);
}
private void cleanupIfIdle() {
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY) && !ResourceRegistry.isBound(JDBC_SUSPENDED_KEY)) {
ResourceRegistry.cleanup();
}
}
private void resumeIfNeeded(JdbcTxStatus status) {
JdbcTxStatus suspended = status.suspended();
if (suspended == null) {
suspended = ResourceRegistry.getOrNull(JDBC_SUSPENDED_KEY, JdbcTxStatus.class);
}
if (suspended != null) {
ResourceRegistry.unbind(JDBC_SUSPENDED_KEY);
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
}
}
private void silentClose(Connection connection) {
if (connection == null) {
return;
}
try {
connection.close();
} catch (SQLException ignored) {
}
}
}
@@ -37,6 +37,9 @@ class JdbcTxStatus implements TxStatus {
@Override
public <R> R resource(Class<R> type) {
if (connection == null) {
throw new IllegalStateException("No connection bound to this transaction status");
}
return type.cast(connection);
}
@@ -118,7 +118,7 @@ public class OpenApiExtension implements FlashExtension {
"});\n" +
"</script>\n" +
"</body>\n" +
"</html>";
"</html>";
}
private static void addOperationFromEvent(OpenApiBuilder builder, RouteEvent event) {
@@ -12,6 +12,7 @@ import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.GET;
import dev.relism.flash.routing.Middleware;
import dev.relism.flash.websocket.WebSocketEndpoint;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -165,6 +166,11 @@ class OpenApiExtensionTest {
routes.put(method.name() + " " + path, handler);
}
@Override
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
}
@Override
protected void addMiddleware(Middleware mw) {
middlewares.add(mw);
@@ -5,6 +5,7 @@ import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.GET;
import dev.relism.flash.websocket.WebSocketEndpoint;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
@@ -172,6 +173,11 @@ class JteExtensionTest {
routes.put(method.name() + " " + path, handler);
}
@Override
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
}
@Override
protected void addMiddleware(dev.relism.flash.routing.Middleware mw) {
mws.add(mw);