feat: introduce WebSocket support with new endpoints and transaction propagation enhancements
This commit is contained in:
+77
-87
@@ -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(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+88
-19
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user