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-jdbc</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>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</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>
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
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;
|
||||
|
||||
public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
||||
|
||||
private final String table;
|
||||
private final String idColumn;
|
||||
|
||||
protected JdbcRepository(String table, String idColumn) {
|
||||
this.table = table;
|
||||
this.idColumn = idColumn;
|
||||
}
|
||||
|
||||
protected Connection connection() {
|
||||
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 String insertSql();
|
||||
protected abstract String updateSql();
|
||||
|
||||
// ── Repository abstract impl ──────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected Optional<T> doFindById(ID id) {
|
||||
return queryOne("select * from " + table + " where " + idColumn + " = ?",
|
||||
ps -> ps.setObject(1, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll() {
|
||||
return queryMany("select * from " + table, ps -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(int page, int size) {
|
||||
return queryMany("select * from " + table + " limit ? offset ?", ps -> {
|
||||
ps.setInt(1, size);
|
||||
ps.setInt(2, page * size);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(Sort sort) {
|
||||
return queryMany("select * from " + table + orderClause(sort), ps -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> doFindAll(int page, int size, Sort sort) {
|
||||
return queryMany("select * from " + table + orderClause(sort) + " limit ? offset ?",
|
||||
ps -> {
|
||||
ps.setInt(1, size);
|
||||
ps.setInt(2, page * size);
|
||||
});
|
||||
}
|
||||
|
||||
@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) {
|
||||
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); }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T doUpdate(T entity) {
|
||||
try (PreparedStatement ps = connection().prepareStatement(updateSql())) {
|
||||
bindUpdate(ps, entity);
|
||||
ps.executeUpdate();
|
||||
return entity;
|
||||
} catch (SQLException e) { throw new TxException(e); }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDelete(T entity) {
|
||||
throw new UnsupportedOperationException("Override doDelete() or use deleteById()");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDeleteById(ID 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long doCount() {
|
||||
return queryOne("select count(*) from " + table, ps -> {},
|
||||
rs -> rs.getLong(1)).orElse(0L);
|
||||
}
|
||||
|
||||
// ── Query helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
protected Optional<T> queryOne(String sql, SqlBinder params) {
|
||||
List<T> r = queryMany(sql, params);
|
||||
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
|
||||
}
|
||||
|
||||
protected <R> Optional<R> queryOne(String sql, SqlBinder params,
|
||||
SqlMapper<R> mapper) {
|
||||
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); }
|
||||
}
|
||||
|
||||
protected List<T> queryMany(String sql, SqlBinder params) {
|
||||
try (PreparedStatement ps = connection().prepareStatement(sql)) {
|
||||
params.bind(ps);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
List<T> results = new ArrayList<>();
|
||||
while (rs.next()) results.add(mapRow(rs));
|
||||
return results;
|
||||
}
|
||||
} 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); }
|
||||
}
|
||||
|
||||
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(", "));
|
||||
}
|
||||
|
||||
@FunctionalInterface public interface SqlBinder { void bind(PreparedStatement ps) throws SQLException; }
|
||||
@FunctionalInterface public interface SqlMapper<R> { R map(ResultSet rs) throws SQLException; }
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package dev.relism.flash.ext.data.jdbc;
|
||||
|
||||
import dev.relism.flash.ext.data.core.*;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class JdbcTxManager implements TxManager {
|
||||
private static final TxResourceKey JDBC_STATUS_KEY = TxResourceKey.of("jdbc.tx.status");
|
||||
|
||||
private final DataSource ds;
|
||||
|
||||
public JdbcTxManager(DataSource ds) {
|
||||
this.ds = Objects.requireNonNull(ds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxStatus begin(TxDefinition definition) {
|
||||
return switch (definition.propagation()) {
|
||||
case REQUIRED -> ResourceRegistry.isBound(JDBC_STATUS_KEY)
|
||||
? joinExisting(definition)
|
||||
: beginNew(definition);
|
||||
case REQUIRES_NEW -> beginNew(definition);
|
||||
case MANDATORY -> {
|
||||
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY))
|
||||
throw new IllegalStateException("MANDATORY: no active transaction");
|
||||
yield joinExisting(definition);
|
||||
}
|
||||
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
|
||||
};
|
||||
}
|
||||
|
||||
private TxStatus beginNew(TxDefinition definition) {
|
||||
try {
|
||||
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
|
||||
if (suspended != null) {
|
||||
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
||||
}
|
||||
Connection conn = ds.getConnection();
|
||||
conn.setAutoCommit(false);
|
||||
if (definition.readOnly()) conn.setReadOnly(true);
|
||||
if (definition.isolation() != TransactionIsolation.DEFAULT) {
|
||||
conn.setTransactionIsolation(definition.isolation().level());
|
||||
}
|
||||
JdbcTxStatus status = new JdbcTxStatus(
|
||||
conn,
|
||||
true,
|
||||
definition.readOnly(),
|
||||
suspended,
|
||||
new JdbcTxStatus.RollbackMarker()
|
||||
);
|
||||
ResourceRegistry.bind(JDBC_STATUS_KEY, status);
|
||||
return status;
|
||||
} catch (SQLException e) {
|
||||
throw new TxException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private TxStatus joinExisting(TxDefinition definition) {
|
||||
JdbcTxStatus existing = ResourceRegistry.get(JDBC_STATUS_KEY, JdbcTxStatus.class);
|
||||
if (definition.readOnly() && !existing.isReadOnly()) {
|
||||
throw new TxException("Cannot join read-write tx as read-only");
|
||||
}
|
||||
return new JdbcTxStatus(
|
||||
existing.connection(),
|
||||
false,
|
||||
definition.readOnly(),
|
||||
null,
|
||||
existing.rollbackMarker()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(TxStatus status) {
|
||||
JdbcTxStatus s = (JdbcTxStatus) status;
|
||||
if (!s.isNewTransaction()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (s.isRollbackOnly()) {
|
||||
s.connection().rollback();
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
||||
return;
|
||||
}
|
||||
s.connection().commit();
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.COMMITTED);
|
||||
} catch (SQLException e) {
|
||||
throw new TxException(e);
|
||||
} finally {
|
||||
cleanupAndResume(s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(TxStatus status) {
|
||||
JdbcTxStatus s = (JdbcTxStatus) status;
|
||||
if (!s.isNewTransaction()) {
|
||||
s.markRollbackOnly();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
s.connection().rollback();
|
||||
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
||||
} catch (SQLException e) {
|
||||
throw new TxException(e);
|
||||
} finally {
|
||||
cleanupAndResume(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupAndResume(JdbcTxStatus status) {
|
||||
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
||||
try {
|
||||
status.connection().close();
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
JdbcTxStatus suspended = status.suspended();
|
||||
if (suspended != null) {
|
||||
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package dev.relism.flash.ext.data.jdbc;
|
||||
|
||||
import dev.relism.flash.ext.data.core.TxStatus;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.Objects;
|
||||
|
||||
class JdbcTxStatus implements TxStatus {
|
||||
static final class RollbackMarker {
|
||||
boolean rollbackOnly;
|
||||
}
|
||||
|
||||
private final Connection connection;
|
||||
private final boolean newTransaction;
|
||||
private final boolean readOnly;
|
||||
private final JdbcTxStatus suspended;
|
||||
private final RollbackMarker rollbackMarker;
|
||||
|
||||
JdbcTxStatus(
|
||||
Connection connection,
|
||||
boolean newTransaction,
|
||||
boolean readOnly,
|
||||
JdbcTxStatus suspended,
|
||||
RollbackMarker rollbackMarker
|
||||
) {
|
||||
this.connection = Objects.requireNonNull(connection);
|
||||
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(connection);
|
||||
}
|
||||
|
||||
Connection connection() { return connection; }
|
||||
JdbcTxStatus suspended() { return suspended; }
|
||||
RollbackMarker rollbackMarker() { return rollbackMarker; }
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package dev.relism.flash.ext.data.jdbc;
|
||||
|
||||
import dev.relism.flash.ext.data.core.*;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class JdbcTxManagerTest {
|
||||
private final JdbcTxManager manager = new JdbcTxManager(dataSource());
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
ResourceRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void required_starts_new_when_absent() {
|
||||
TxStatus s = manager.begin(TxDefinition.DEFAULTS);
|
||||
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(Connection.class), inner.resource(Connection.class));
|
||||
assertFalse(inner.isNewTransaction());
|
||||
manager.rollback(outer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requires_new_creates_distinct_connection() {
|
||||
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
|
||||
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW));
|
||||
assertNotSame(outer.resource(Connection.class), inner.resource(Connection.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);
|
||||
}
|
||||
|
||||
private static DataSource dataSource() {
|
||||
return new DataSource() {
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return DriverManager.getConnection("jdbc:h2:mem:tx-jdbc;DB_CLOSE_DELAY=-1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
return DriverManager.getConnection("jdbc:h2:mem:tx-jdbc;DB_CLOSE_DELAY=-1", username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> iface) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(Class<?> iface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.PrintWriter getLogWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLogWriter(java.io.PrintWriter out) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoginTimeout(int seconds) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoginTimeout() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.logging.Logger getParentLogger() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user