refactor: rename packages and files to use 'flash' prefix for consistency
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-extensions</artifactId>
|
||||
<version>1.1-indev6</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>flash-ext-data-hibernate</artifactId>
|
||||
|
||||
<properties>
|
||||
<jacoco.version>0.8.12</jacoco.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-ext-data-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco.version}</version>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.80</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package dev.relism.flash.ext.data.hibernate;
|
||||
|
||||
import dev.relism.flash.ext.data.core.*;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.query.MutationQuery;
|
||||
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
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> {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
protected HibernateRepository(Class<T> type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
// ── Session — always safe, tx() wrapper guarantees active transaction ─────
|
||||
|
||||
protected Session session() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(int page, int size) {
|
||||
return hql("from " + type.getSimpleName())
|
||||
.setFirstResult(page * size)
|
||||
.setMaxResults(size)
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(Sort sort) {
|
||||
return hql("from " + type.getSimpleName() + orderClause(sort))
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(int page, int size, Sort sort) {
|
||||
return hql("from " + type.getSimpleName() + orderClause(sort))
|
||||
.setFirstResult(page * size)
|
||||
.setMaxResults(size)
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T doSave(T entity) {
|
||||
session().persist(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T doUpdate(T entity) {
|
||||
return session().merge(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDelete(T entity) {
|
||||
Session s = session();
|
||||
s.remove(s.contains(entity) ? entity : s.merge(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDeleteById(ID id) {
|
||||
doFindById(id).ifPresent(this::doDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doExistsById(ID id) {
|
||||
return doFindById(id).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long doCount() {
|
||||
return session()
|
||||
.createQuery("select count(*) from " + type.getSimpleName(), Long.class)
|
||||
.uniqueResultOptional()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
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);
|
||||
params.accept(q);
|
||||
return q.setFirstResult(page * size).setMaxResults(size).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(() -> {
|
||||
MutationQuery q = session().createMutationQuery(hql);
|
||||
params.accept(q);
|
||||
return q.executeUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
protected Class<T> entityType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private String orderClause(Sort sort) {
|
||||
return " order by " + sort.columns().stream()
|
||||
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package dev.relism.flash.ext.data.hibernate;
|
||||
|
||||
import dev.relism.flash.ext.data.core.*;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class HibernateTxManager implements TxManager {
|
||||
private static final TxResourceKey HIBERNATE_STATUS_KEY = TxResourceKey.of("hibernate.tx.status");
|
||||
|
||||
private final SessionFactory sf;
|
||||
|
||||
public HibernateTxManager(SessionFactory sessionFactory) {
|
||||
this.sf = Objects.requireNonNull(sessionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxStatus begin(TxDefinition definition) {
|
||||
return switch (definition.propagation()) {
|
||||
case REQUIRED -> ResourceRegistry.isBound(HIBERNATE_STATUS_KEY)
|
||||
? joinExisting(definition)
|
||||
: beginNew(definition);
|
||||
case REQUIRES_NEW -> beginNew(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");
|
||||
};
|
||||
}
|
||||
|
||||
private TxStatus beginNew(TxDefinition definition) {
|
||||
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
|
||||
if (suspended != null) {
|
||||
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
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) {
|
||||
HibernateTxStatus existing = ResourceRegistry.get(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
|
||||
if (definition.readOnly() && !existing.isReadOnly()) {
|
||||
throw new TxException("Cannot join read-write tx as read-only");
|
||||
}
|
||||
return new HibernateTxStatus(
|
||||
existing.session(),
|
||||
false,
|
||||
definition.readOnly(),
|
||||
null,
|
||||
existing.rollbackMarker()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(TxStatus status) {
|
||||
HibernateTxStatus s = (HibernateTxStatus) status;
|
||||
if (!s.isNewTransaction()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (s.isRollbackOnly() && s.session().getTransaction().isActive()) {
|
||||
s.session().getTransaction().rollback();
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
||||
} else {
|
||||
s.session().getTransaction().commit();
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.COMMITTED);
|
||||
}
|
||||
} finally {
|
||||
cleanupAndResume(s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(TxStatus status) {
|
||||
HibernateTxStatus s = (HibernateTxStatus) status;
|
||||
if (!s.isNewTransaction()) {
|
||||
s.markRollbackOnly();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (s.session().getTransaction().isActive()) {
|
||||
s.session().getTransaction().rollback();
|
||||
}
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
||||
} finally {
|
||||
cleanupAndResume(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupAndResume(HibernateTxStatus status) {
|
||||
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
||||
status.session().close();
|
||||
HibernateTxStatus suspended = status.suspended();
|
||||
if (suspended != null) {
|
||||
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package dev.relism.flash.ext.data.hibernate;
|
||||
|
||||
import dev.relism.flash.ext.data.core.TxStatus;
|
||||
import org.hibernate.Session;
|
||||
|
||||
class HibernateTxStatus implements TxStatus {
|
||||
static final class RollbackMarker {
|
||||
boolean rollbackOnly;
|
||||
}
|
||||
|
||||
private final Session session;
|
||||
private final boolean newTransaction;
|
||||
private final boolean readOnly;
|
||||
private final HibernateTxStatus suspended;
|
||||
private final RollbackMarker rollbackMarker;
|
||||
|
||||
HibernateTxStatus(
|
||||
Session session,
|
||||
boolean newTransaction,
|
||||
boolean readOnly,
|
||||
HibernateTxStatus suspended,
|
||||
RollbackMarker rollbackMarker
|
||||
) {
|
||||
this.session = session;
|
||||
this.newTransaction = newTransaction;
|
||||
this.readOnly = readOnly;
|
||||
this.suspended = suspended;
|
||||
this.rollbackMarker = rollbackMarker;
|
||||
}
|
||||
|
||||
@Override public boolean isNewTransaction() { return newTransaction; }
|
||||
@Override public boolean isReadOnly() { return readOnly; }
|
||||
@Override public boolean isRollbackOnly() { return rollbackMarker.rollbackOnly; }
|
||||
@Override public void markRollbackOnly() { rollbackMarker.rollbackOnly = true; }
|
||||
|
||||
@Override
|
||||
public <R> R resource(Class<R> type) {
|
||||
return type.cast(session);
|
||||
}
|
||||
|
||||
Session session() { return session; }
|
||||
HibernateTxStatus suspended() { return suspended; }
|
||||
RollbackMarker rollbackMarker() { return rollbackMarker; }
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package dev.relism.flash.ext.data.hibernate;
|
||||
|
||||
import dev.relism.flash.ext.data.core.*;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HibernateTxManagerTest {
|
||||
static SessionFactory sf;
|
||||
static HibernateTxManager manager;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
sf = TestHelper.buildSessionFactory();
|
||||
manager = new HibernateTxManager(sf);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void teardown() {
|
||||
if (sf != null) {
|
||||
sf.close();
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
ResourceRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void required_starts_new_when_absent() {
|
||||
TxStatus s = manager.begin(TxDefinition.DEFAULTS);
|
||||
assertNotNull(s.resource(Session.class));
|
||||
assertTrue(s.isNewTransaction());
|
||||
assertDoesNotThrow(() -> manager.commit(s));
|
||||
}
|
||||
|
||||
@Test
|
||||
void required_joins_existing_when_present() {
|
||||
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
|
||||
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRED));
|
||||
assertSame(outer.resource(Session.class), inner.resource(Session.class));
|
||||
manager.rollback(outer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requires_new_uses_separate_session() {
|
||||
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
|
||||
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW));
|
||||
assertNotSame(outer.resource(Session.class), inner.resource(Session.class));
|
||||
manager.commit(inner);
|
||||
manager.rollback(outer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollback_on_joined_marks_outer_rollback_only() {
|
||||
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
|
||||
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRED));
|
||||
manager.rollback(inner);
|
||||
assertTrue(outer.isRollbackOnly());
|
||||
manager.rollback(outer);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package dev.relism.flash.ext.data.hibernate;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.boot.MetadataSources;
|
||||
import org.hibernate.boot.registry.StandardServiceRegistry;
|
||||
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
|
||||
|
||||
public class TestHelper {
|
||||
public static SessionFactory buildSessionFactory() {
|
||||
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
|
||||
.applySetting("hibernate.connection.url", "jdbc:h2:mem:tx-hibernate;DB_CLOSE_DELAY=-1")
|
||||
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
|
||||
.applySetting("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
|
||||
.applySetting("hibernate.hbm2ddl.auto", "none")
|
||||
.applySetting("hibernate.show_sql", "false")
|
||||
.build();
|
||||
try {
|
||||
return new MetadataSources(registry).buildMetadata().buildSessionFactory();
|
||||
} catch (Exception e) {
|
||||
StandardServiceRegistryBuilder.destroy(registry);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user