diff --git a/.github/workflows/publish-javadoc.yml b/.github/workflows/publish-javadoc.yml
new file mode 100644
index 0000000..ca89052
--- /dev/null
+++ b/.github/workflows/publish-javadoc.yml
@@ -0,0 +1,38 @@
+name: Deploy Publish JavaDoc
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Temurin 21
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 21
+ cache: maven
+
+ - name: Build release artifacts
+ run: mvn -B -DskipTests clean verify
+
+ - name: Generate aggregated JavaDoc
+ run: mvn -B -DskipTests -pl flash,flash-extensions -am javadoc:aggregate
+
+ - name: Publish JavaDoc
+ uses: MathieuSoysal/Javadoc-publisher.yml@v3.0.2
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ javadoc-branch: javadoc
+ java-version: 21
+ target-folder: javadoc
+ project: maven
+ custom-command: mvn -B -DskipTests -pl flash,flash-extensions -am javadoc:aggregate -DskipTests
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 49a3dae..ffe5512 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,292 +4,95 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -298,7 +101,7 @@
-
+
@@ -341,64 +144,64 @@
- {
- "keyToString": {
- "Application.(dev) flash-bench.executor": "Run",
- "Application.ExternalBenchmark (1).executor": "Run",
- "Application.ExternalBenchmark.executor": "Run",
- "Application.Main.executor": "Run",
- "Application.MainAlt.executor": "Run",
- "Application.dev.relism.bench.Main.executor": "Run",
- "JUnit.RequestParserTest.executor": "Run",
- "JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
- "Maven.FlashPractice [test].executor": "Run",
- "Maven.flash [compile].executor": "Run",
- "Maven.flash [install].executor": "Run",
- "Maven.flash [test].executor": "Run",
- "Maven.flash [verify].executor": "Run",
- "Maven.flash-bench [clean].executor": "Run",
- "Maven.flash-bench [install].executor": "Run",
- "Maven.flash-bench [package].executor": "Run",
- "Maven.flash-bench [validate].executor": "Run",
- "Maven.flash-ext-limiter [install].executor": "Run",
- "Maven.flash-ext-limiter [package].executor": "Run",
- "Maven.flash-ext-view [verify].executor": "Run",
- "Maven.flash-parent [clean].executor": "Run",
- "Maven.flash-parent [compile].executor": "Run",
- "Maven.flash-parent [deploy].executor": "Run",
- "Maven.flash-parent [install].executor": "Run",
- "Maven.flash-parent [package].executor": "Run",
- "Maven.flash-parent [test].executor": "Run",
- "Maven.flash-parent [validate].executor": "Run",
- "Maven.flash-parent [verify].executor": "Run",
- "Maven.flash-web-bundler [test].executor": "Run",
- "ModuleVcsDetector.initialDetectionPerformed": "true",
- "RunOnceActivity.MCP Project settings loaded": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
- "RunOnceActivity.git.unshallow": "true",
- "RunOnceActivity.typescript.service.memoryLimit.init": "true",
- "SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "codeWithMe.voiceChat.enabledByDefault": "false",
- "git-widget-placeholder": "master",
- "ignore.virus.scanning.warn.message": "true",
- "kotlin-language-version-configured": "true",
- "last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "npm.build.executor": "Run",
- "onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash/flash-extensions/flash-ext-data/src/main/java/dev/relism/Main.java",
- "project.structure.last.edited": "Modules",
- "project.structure.proportion": "0.15",
- "project.structure.side.proportion": "0.1150748",
- "settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
- "ts.external.directory.path": "C:\\Users\\elorc\\Documents\\Coding\\Java\\practice\\Flash\\nuxt-shadcn-dashboard\\node_modules\\typescript\\lib",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
@@ -534,7 +337,16 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -632,7 +444,23 @@
1776724331443
-
+
+
+ 1777199691803
+
+
+
+ 1777199691804
+
+
+
+ 1777451475753
+
+
+
+ 1777451475753
+
+
@@ -674,7 +502,11 @@
-
+
+
+
+
+
diff --git a/benchmarks/websocket/benchmark.yml b/benchmarks/websocket/benchmark.yml
new file mode 100644
index 0000000..ce30a3d
--- /dev/null
+++ b/benchmarks/websocket/benchmark.yml
@@ -0,0 +1,24 @@
+config:
+ target: "ws://localhost:8080/echo"
+ engines:
+ ws: {}
+ phases:
+ - duration: 30
+ arrivalRate: 50
+ rampTo: 500
+ name: "Riscaldamento progressivo"
+ - duration: 120
+ arrivalRate: 1000 # 1000 nuovi utenti al secondo
+ name: "Carico Estremo"
+ ensure:
+ maxErrorRate: 5
+ p99: 150
+
+scenarios:
+ - name: "Saturazione Totale"
+ engine: ws
+ flow:
+ - loop:
+ - send: "Benchmark data"
+ # Rimosso il 'think' per eliminare il limite artificiale di 10msg/s per utente
+ count: 100 # Ogni utente spara a raffica 100 messaggi senza pause
\ No newline at end of file
diff --git a/flash-extensions/flash-ext-data-core/docs/README.md b/flash-extensions/flash-ext-data-core/docs/README.md
new file mode 100644
index 0000000..8e19234
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/docs/README.md
@@ -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`: base repository auto-transazionale.
+- `Spec`: predicato componibile.
+- `Query`: oggetto query con spec, sort e paging.
+- `SpecBuilder`: DSL fluente per costruire spec tipizzate.
+- `RepositorySupport`: 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)`
+- `doFindOne(Spec)`
+- `doFindPage(Query)`
+- `doDeleteAll(Spec)`
+- `doUpdateAll(Spec, T)`
+
+I vecchi overload di `findAll(...)` e `findPage(...)` sono stati ridotti a una combinazione di `Query` e `Spec`.
+
+```java
+public abstract class Repository {
+ protected Repository(Tx tx) { ... }
+ protected final R tx(Tx.TxCallable 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.
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java
index 63ace83..60ad68d 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java
@@ -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;
};
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Query.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Query.java
new file mode 100644
index 0000000..a42a5d0
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Query.java
@@ -0,0 +1,28 @@
+package dev.relism.flash.ext.data.core;
+
+public record Query(Spec spec, Sort sort, Integer page, Integer size) {
+ public Query {
+ spec = spec == null ? Spec.all() : spec;
+ sort = sort == null ? Sort.unsorted() : sort;
+ }
+
+ public static Query all() {
+ return new Query<>(Spec.all(), Sort.unsorted(), null, null);
+ }
+
+ public Query where(Spec spec) {
+ return new Query<>(spec, sort, page, size);
+ }
+
+ public Query orderBy(Sort sort) {
+ return new Query<>(spec, sort, page, size);
+ }
+
+ public Query page(int page, int size) {
+ return new Query<>(spec, sort, page, size);
+ }
+
+ public boolean isPaged() {
+ return page != null && size != null;
+ }
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java
index 6da65d8..242edee 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java
@@ -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 {
+public abstract class Repository extends RepositorySupport {
- private final TxDefinition required = TxDefinition.DEFAULTS
- .withPropagation(TransactionPropagation.REQUIRED);
-
- // ── CRUD ──────────────────────────────────────────────────────────────────
+ protected Repository(Tx tx) {
+ super(tx);
+ }
public Optional findById(ID id) {
- return tx(() -> doFindById(id));
- }
-
- public List findAll() {
- return tx(this::doFindAll);
- }
-
- public List findAll(int page, int size) {
- return tx(() -> doFindAll(page, size));
- }
-
- public List findAll(Sort sort) {
- return tx(() -> doFindAll(sort));
- }
-
- public List findAll(int page, int size, Sort sort) {
- return tx(() -> doFindAll(page, size, sort));
- }
-
- public Page findPage(int page, int size) {
- return tx(() -> doFindPage(page, size));
- }
-
- public Page findPage(int page, int size, Sort sort) {
- return tx(() -> doFindPage(page, size, sort));
- }
-
- public T save(T entity) {
- return tx(() -> doSave(entity));
- }
-
- public List saveAll(Iterable entities) {
- return tx(() -> {
- List 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 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 tx(Tx.TxCallable work) {
- return Tx.call(required, work);
+ public List findAll() {
+ return findAll(Query.all());
}
- // ── Abstract — implemented by HibernateRepository / JdbcRepository ────────
+ public List findAll(Spec spec) {
+ return findAll(Query.all().where(spec));
+ }
+
+ public List findAll(Query query) {
+ return roQuery(() -> doFind(query));
+ }
+
+ public Page findPage(Query query) {
+ return roQuery(() -> doFindPage(query));
+ }
+
+ public Optional findOne(Spec 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 saveAll(Iterable 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 spec) {
+ return rwQuery(() -> doDeleteAll(spec));
+ }
+
+ public int updateAll(Spec spec, T patch) {
+ return rwQuery(() -> doUpdateAll(spec, patch));
+ }
+
+ public List findAll(int page, int size) {
+ return findAll(Query.all().page(page, size));
+ }
+
+ public List findAll(Sort sort) {
+ return findAll(Query.all().orderBy(sort));
+ }
+
+ public List findAll(int page, int size, Sort sort) {
+ return findAll(Query.all().orderBy(sort).page(page, size));
+ }
+
+ public Page findPage(int page, int size) {
+ return findPage(Query.all().page(page, size));
+ }
+
+ public Page findPage(int page, int size, Sort sort) {
+ return findPage(Query.all().orderBy(sort).page(page, size));
+ }
+
+ public void deleteAll(Iterable entities) {
+ rwQuery(() -> {
+ for (T entity : entities) {
+ doDelete(entity);
+ }
+ return null;
+ });
+ }
protected abstract Optional doFindById(ID id);
- protected abstract List doFindAll();
- protected abstract List doFindAll(int page, int size);
- protected abstract List doFindAll(Sort sort);
- protected abstract List doFindAll(int page, int size, Sort sort);
- protected abstract Page doFindPage(int page, int size);
- protected abstract Page 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();
-}
\ No newline at end of file
+ protected abstract List doFind(Query query);
+ protected abstract Optional doFindOne(Spec spec);
+ protected abstract Page doFindPage(Query query);
+ protected abstract boolean doExistsById(ID id);
+ protected abstract long doCount();
+ protected abstract T doSave(T entity);
+ protected abstract List doSaveAll(Iterable entities);
+ protected abstract T doUpdate(T entity);
+ protected abstract void doDelete(T entity);
+ protected abstract void doDeleteById(ID id);
+ protected abstract int doDeleteAll(Spec spec);
+ protected abstract int doUpdateAll(Spec spec, T patch);
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/RepositorySupport.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/RepositorySupport.java
new file mode 100644
index 0000000..7dd78ec
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/RepositorySupport.java
@@ -0,0 +1,24 @@
+package dev.relism.flash.ext.data.core;
+
+public abstract class RepositorySupport {
+ 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 roQuery(Tx.TxCallable work) {
+ return tx.call(ro, work);
+ }
+
+ protected final R rwQuery(Tx.TxCallable work) {
+ return tx.call(rw, work);
+ }
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java
index e0efae5..5b491f0 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java
@@ -31,6 +31,11 @@ public final class ResourceRegistry {
SYNCHRONIZATIONS.get().clear();
}
+ public static void cleanup() {
+ RESOURCES.remove();
+ SYNCHRONIZATIONS.remove();
+ }
+
public static R get(TxResourceKey key, Class type) {
Object value = RESOURCES.get().get(key);
if (value == null) {
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java
index e4d8fbd..8a4575d 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java
@@ -7,6 +7,10 @@ public record Sort(List 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 columns) {
next.add(new Column(column, asc));
return new Sort(next);
}
-}
\ No newline at end of file
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Spec.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Spec.java
new file mode 100644
index 0000000..315378c
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Spec.java
@@ -0,0 +1,26 @@
+package dev.relism.flash.ext.data.core;
+
+@FunctionalInterface
+public interface Spec {
+ String toFragment(SpecContext ctx);
+
+ default Spec and(Spec other) {
+ return ctx -> "(" + this.toFragment(ctx) + " AND " + other.toFragment(ctx) + ")";
+ }
+
+ default Spec or(Spec other) {
+ return ctx -> "(" + this.toFragment(ctx) + " OR " + other.toFragment(ctx) + ")";
+ }
+
+ default Spec not() {
+ return ctx -> "NOT (" + this.toFragment(ctx) + ")";
+ }
+
+ static Spec all() {
+ return ctx -> "1=1";
+ }
+
+ static Spec none() {
+ return ctx -> "1=0";
+ }
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecBuilder.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecBuilder.java
new file mode 100644
index 0000000..88305e1
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecBuilder.java
@@ -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 {
+ private SpecBuilder() {}
+
+ public static FieldSpec field(String column) {
+ return new FieldSpec<>(column);
+ }
+
+ public static final class FieldSpec {
+ private final String column;
+
+ private FieldSpec(String column) {
+ this.column = Objects.requireNonNull(column);
+ }
+
+ public Spec eq(V value) { return ctx -> column + " = " + ctx.bind(value); }
+ public Spec neq(V value) { return ctx -> column + " != " + ctx.bind(value); }
+ public Spec like(String pattern) { return ctx -> column + " like " + ctx.bind(pattern); }
+ public Spec isNull() { return ctx -> column + " is null"; }
+ public Spec isNotNull() { return ctx -> column + " is not null"; }
+
+ public Spec in(Collection values) {
+ return ctx -> column + " in (" + values.stream().map(ctx::bind).collect(Collectors.joining(", ")) + ")";
+ }
+
+ public > Spec gt(C value) { return ctx -> column + " > " + ctx.bind(value); }
+ public > Spec lt(C value) { return ctx -> column + " < " + ctx.bind(value); }
+ public > Spec between(C lo, C hi) {
+ return ctx -> column + " between " + ctx.bind(lo) + " and " + ctx.bind(hi);
+ }
+ }
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecContext.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecContext.java
new file mode 100644
index 0000000..2d0cd7f
--- /dev/null
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecContext.java
@@ -0,0 +1,5 @@
+package dev.relism.flash.ext.data.core;
+
+public interface SpecContext {
+ String bind(Object value);
+}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java
index 48510d1..de58a55 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java
@@ -3,6 +3,7 @@ package dev.relism.flash.ext.data.core;
public enum TransactionPropagation {
REQUIRED,
REQUIRES_NEW,
+ SUPPORTS,
NOT_SUPPORTED,
MANDATORY
}
diff --git a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java
index 06241c9..facfa43 100644
--- a/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java
+++ b/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java
@@ -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> 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 call(TxCallable work) {
+ public T call(TxCallable work) {
return call(TxDefinition.DEFAULTS, work);
}
- public static T call(TxDefinition definition, TxCallable work) {
- TxStatus status = manager().begin(definition);
+ public T call(TxDefinition definition, TxCallable 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 resource(Class type) {
+ public R resource(Class 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 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 RuntimeException sneakyThrow(Throwable t) throws E {
+ throw (E) t;
+ }
+
@FunctionalInterface
public interface TxRunnable {
void run();
diff --git a/flash-extensions/flash-ext-data-hibernate/docs/README.md b/flash-extensions/flash-ext-data-hibernate/docs/README.md
new file mode 100644
index 0000000..5264a02
--- /dev/null
+++ b/flash-extensions/flash-ext-data-hibernate/docs/README.md
@@ -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 l’estensione in Flash
+
+L’estensione 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 {
+ 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 {
+ public static final SpecBuilder.FieldSpec EMAIL = SpecBuilder.field("u.email");
+ public static final SpecBuilder.FieldSpec ACTIVE = SpecBuilder.field("u.active");
+
+ public UserRepository(Tx tx) {
+ super(tx, User.class);
+ }
+
+ public Optional findByEmail(String email) {
+ return findOne(EMAIL.eq(email));
+ }
+}
+```
+
+Le query domain-specific possono usare gli helper della base class:
+
+```java
+public List 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)` e `updateAll(Spec, 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.
diff --git a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java
index 0e94894..3ea99ee 100644
--- a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java
+++ b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java
@@ -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
- extends Repository {
+public abstract class HibernateRepository extends Repository {
private final Class type;
- protected HibernateRepository(Class type) {
+ protected HibernateRepository(Tx tx, Class 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 doFindById(ID id) {
return Optional.ofNullable(session().get(type, id));
}
@Override
- protected List doFindAll() {
- return hql("from " + type.getSimpleName()).getResultList();
+ protected List doFind(Query 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 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 doFindAll(int page, int size) {
- return hql("from " + type.getSimpleName())
- .setFirstResult(page * size)
- .setMaxResults(size)
- .getResultList();
+ protected Optional doFindOne(Spec spec) {
+ return doFind(Query.all().where(spec).page(0, 1)).stream().findFirst();
}
@Override
- protected List doFindAll(Sort sort) {
- return hql("from " + type.getSimpleName() + orderClause(sort))
- .getResultList();
+ protected Page doFindPage(Query query) {
+ if (!query.isPaged()) {
+ throw new IllegalArgumentException("Paged query requires page and size");
+ }
+ long total = countWhere(query.spec());
+ List content = doFind(query);
+ return new Page<>(content, query.page(), query.size(), total);
}
@Override
- protected List 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 doFindPage(int page, int size) {
- long total = doCount();
- return new Page<>(doFindAll(page, size), page, size, total);
- }
-
- @Override
- protected Page 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
return entity;
}
+ @Override
+ protected List doSaveAll(Iterable entities) {
+ List 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
}
@Override
- protected boolean doExistsById(ID id) {
- return doFindById(id).isPresent();
+ protected int doDeleteAll(Spec 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 spec, T patch) {
+ throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
}
- // ── Query helpers — usabili nelle sottoclassi domain ─────────────────────
-
- protected TypedQuery hql(String hql) {
- return session().createQuery(hql, type);
- }
-
- protected TypedQuery hql(String hql, Class resultType) {
- return session().createQuery(hql, resultType);
- }
-
- protected Optional findOne(String hql, Consumer> params) {
- TypedQuery q = hql(hql);
- params.accept(q);
- return q.getResultStream().findFirst();
- }
-
- protected List findMany(String hql, Consumer> params) {
- return tx(() -> {
- TypedQuery q = hql(hql);
+ protected List hql(String hql, Consumer> params) {
+ return roQuery(() -> {
+ TypedQuery q = session().createQuery(hql, type);
params.accept(q);
return q.getResultList();
});
}
- protected List findMany(String hql, Consumer> params,
- int page, int size) {
- return tx(() -> {
- TypedQuery q = hql(hql);
+ protected List hql(String hql, Class resultType, Consumer> params) {
+ return roQuery(() -> {
+ TypedQuery q = session().createQuery(hql, resultType);
params.accept(q);
- return q.setFirstResult(page * size).setMaxResults(size).getResultList();
+ return q.getResultList();
});
}
- protected Page findManyPaged(String hql, String countHql,
- Consumer> params,
- int page, int size) {
- return tx(() -> {
- long total = session()
- .createQuery(countHql, Long.class)
- .uniqueResultOptional()
- .orElse(0L);
- List content = findMany(hql, params, page, size);
- return new Page<>(content, page, size, total);
- });
- }
-
- protected int execute(String hql, Consumer params) {
- return tx(() -> {
+ protected int hqlMutate(String hql, Consumer params) {
+ return rwQuery(() -> {
MutationQuery q = session().createMutationQuery(hql);
params.accept(q);
return q.executeUpdate();
@@ -169,9 +151,17 @@ public abstract class HibernateRepository
return type;
}
+ private long countWhere(Spec spec) {
+ HibernateSpecContext ctx = new HibernateSpecContext();
+ String where = spec != null ? " where " + spec.toFragment(ctx) : "";
+ TypedQuery 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(", "));
}
-}
\ No newline at end of file
+}
diff --git a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateSpecContext.java b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateSpecContext.java
new file mode 100644
index 0000000..c80fed7
--- /dev/null
+++ b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateSpecContext.java
@@ -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 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);
+ }
+}
diff --git a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java
index 5f426e4..c43ca2b 100644
--- a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java
+++ b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java
@@ -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) {
+ }
+ }
}
diff --git a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java
index 6e97c81..5a6a3e6 100644
--- a/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java
+++ b/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java
@@ -35,6 +35,9 @@ class HibernateTxStatus implements TxStatus {
@Override
public R resource(Class type) {
+ if (session == null) {
+ throw new IllegalStateException("No session bound to this transaction status");
+ }
return type.cast(session);
}
diff --git a/flash-extensions/flash-ext-data-jdbc/docs/README.md b/flash-extensions/flash-ext-data-jdbc/docs/README.md
new file mode 100644
index 0000000..62e9cd4
--- /dev/null
+++ b/flash-extensions/flash-ext-data-jdbc/docs/README.md
@@ -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 l’estensione 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 {
+ 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 {
+ public static final SpecBuilder.FieldSpec EMAIL = SpecBuilder.field("email");
+
+ public UserRepository(Tx tx) {
+ super(tx, "users", "id");
+ }
+}
+```
+
+Per il salvataggio e l’update 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)`
+- 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.
diff --git a/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java
index 6a9bc22..10f62ac 100644
--- a/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java
+++ b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java
@@ -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 extends Repository {
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 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 doFindAll() {
- return queryMany("select * from " + table, ps -> {});
- }
+ protected List doFind(Query 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 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 doFindAll(Sort sort) {
- return queryMany("select * from " + table + orderClause(sort), ps -> {});
+ protected Optional doFindOne(Spec spec) {
+ return doFind(Query.all().where(spec).page(0, 1)).stream().findFirst();
}
@Override
- protected List 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 doFindPage(Query query) {
+ if (!query.isPaged()) {
+ throw new IllegalArgumentException("Paged query requires page and size");
+ }
+ long total = countWhere(query.spec());
+ List content = doFind(query);
+ return new Page<>(content, query.page(), query.size(), total);
}
@Override
- protected Page 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 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 doSaveAll(Iterable entities) {
+ List saved = new ArrayList<>();
+ for (T entity : entities) {
+ saved.add(doSave(entity));
+ }
+ return saved;
}
@Override
@@ -92,7 +104,9 @@ public abstract class JdbcRepository extends Repository {
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 extends Repository {
@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 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 spec, T patch) {
+ throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
}
- // ── Query helpers ─────────────────────────────────────────────────────────
-
protected Optional queryOne(String sql, SqlBinder params) {
List r = queryMany(sql, params);
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
}
- protected Optional queryOne(String sql, SqlBinder params,
- SqlMapper mapper) {
+ protected Optional queryOne(String sql, SqlBinder params, SqlMapper 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 queryMany(String sql, SqlBinder params) {
@@ -144,26 +155,47 @@ public abstract class JdbcRepository extends Repository {
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 entityType() {
+ return null;
}
- @FunctionalInterface public interface SqlBinder { void bind(PreparedStatement ps) throws SQLException; }
- @FunctionalInterface public interface SqlMapper { R map(ResultSet rs) throws SQLException; }
-}
\ No newline at end of file
+ private long countWhere(Spec 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 map(ResultSet rs) throws SQLException;
+ }
+}
diff --git a/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcSpecContext.java b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcSpecContext.java
new file mode 100644
index 0000000..8de4ba3
--- /dev/null
+++ b/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcSpecContext.java
@@ -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