Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement transactions #30

Merged
merged 8 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.9.0] - unreleased

## [0.8.0] - 2024-10-20
## [0.8.0] - 2024-11-03

- [PR #27](https://github.com/itsallcode/simple-jdbc/pull/27): Update dependencies
- [PR #28](https://github.com/itsallcode/simple-jdbc/pull/28): Refactored batch inserts (**Breaking change**)
- [PR #29](https://github.com/itsallcode/simple-jdbc/pull/29): Setting values for a `PreparedStatement` (**Breaking change**)
- [PR #30](https://github.com/itsallcode/simple-jdbc/pull/30): Add transaction support

## [0.7.1] - 2024-09-01

Expand Down
86 changes: 86 additions & 0 deletions src/main/java/org/itsallcode/jdbc/DbOperations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.itsallcode.jdbc;

import static java.util.function.Predicate.not;

import java.util.Arrays;

import org.itsallcode.jdbc.resultset.RowMapper;
import org.itsallcode.jdbc.resultset.SimpleResultSet;
import org.itsallcode.jdbc.resultset.generic.Row;

/**
* Interface for various DB operations.
*/
public interface DbOperations extends AutoCloseable {
/**
* Execute all commands in a SQL script, separated with {@code ;}.
*
* @param sqlScript the script to execute.
*/
default void executeScript(final String sqlScript) {
Arrays.stream(sqlScript.split(";"))
.map(String::trim)
.filter(not(String::isEmpty))
.forEach(this::executeStatement);
}

/**
* Execute a single SQL statement.
*
* @param sql the statement
* @param preparedStatementSetter prepared statement setter
*/
void executeStatement(final String sql, PreparedStatementSetter preparedStatementSetter);

/**
* Execute a single SQL statement.
*
* @param sql the statement
*/
void executeStatement(final String sql);

/**
* Execute a SQL query and return a {@link SimpleResultSet result set} with
* generic {@link Row}s.
*
* @param sql the query
* @return the result set
*/
SimpleResultSet<Row> query(final String sql);

/**
* Execute a SQL query and return a {@link SimpleResultSet result set} with rows
* converted to a custom type {@link T}.
*
* @param <T> generic row type
* @param sql SQL query
* @param rowMapper row mapper
* @return the result set
*/
<T> SimpleResultSet<T> query(final String sql, final RowMapper<T> rowMapper);

/**
* Execute a SQL query, set parameters and return a {@link SimpleResultSet
* result set} with rows converted to a custom type {@link T}.
*
* @param <T> generic row type
* @param sql SQL query
* @param preparedStatementSetter the prepared statement setter
* @param rowMapper row mapper
* @return the result set
*/
<T> SimpleResultSet<T> query(final String sql, final PreparedStatementSetter preparedStatementSetter,
final RowMapper<T> rowMapper);

/**
* Create a batch insert builder
*
* @param rowType row type
* @param <T> row type
* @return batch insert builder
*/
<T> BatchInsertBuilder<T> batchInsert(final Class<T> rowType);

@Override
void close();
}
101 changes: 41 additions & 60 deletions src/main/java/org/itsallcode/jdbc/SimpleConnection.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package org.itsallcode.jdbc;

import static java.util.function.Predicate.not;

import java.sql.*;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Logger;

Expand All @@ -17,7 +14,7 @@
* A simplified version of a JDBC {@link Connection}. Create new connections
* with {@link ConnectionFactory#create(String, String, String)}.
*/
public class SimpleConnection implements AutoCloseable {
public class SimpleConnection implements DbOperations {
private static final Logger LOG = Logger.getLogger(SimpleConnection.class.getName());

private final Connection connection;
Expand All @@ -33,65 +30,40 @@ public class SimpleConnection implements AutoCloseable {
}

/**
* Execute all commands in a SQL script, separated with {@code ;}.
* Start a new transaction by disabling auto commit.
*
* @param sqlScript the script to execute.
* @return a new, running transaction.
*/
public void executeScript(final String sqlScript) {
Arrays.stream(sqlScript.split(";"))
.map(String::trim)
.filter(not(String::isEmpty))
.forEach(this::executeStatement);
public Transaction startTransaction() {
return Transaction.start(this);
}

/**
* Execute a single SQL statement.
*
* @param sql the statement
*/
@Override
public void executeStatement(final String sql) {
try (Statement statement = connection.createStatement()) {
statement.execute(sql);
} catch (final SQLException e) {
throw new UncheckedSQLException("Error executing '" + sql + "'", e);
this.executeStatement(sql, stmt -> {
});
}

@Override
public void executeStatement(final String sql, final PreparedStatementSetter preparedStatementSetter) {
try (SimplePreparedStatement statement = prepareStatement(sql)) {
statement.setValues(preparedStatementSetter);
statement.execute();
}
}

/**
* Execute a SQL query and return a {@link SimpleResultSet result set} with
* generic {@link Row}s.
*
* @param sql the query
* @return the result set
*/
@Override
public SimpleResultSet<Row> query(final String sql) {
return query(sql, ContextRowMapper.generic(dialect));
}

/**
* Execute a SQL query and return a {@link SimpleResultSet result set} with rows
* converted to a custom type {@link T}.
*
* @param <T> generic row type
* @param sql SQL query
* @param rowMapper row mapper
* @return the result set
*/
@Override
public <T> SimpleResultSet<T> query(final String sql, final RowMapper<T> rowMapper) {
return query(sql, ps -> {
}, rowMapper);
}

/**
* Execute a SQL query, set parameters and return a {@link SimpleResultSet
* result set} with rows converted to a custom type {@link T}.
*
* @param <T> generic row type
* @param sql SQL query
* @param preparedStatementSetter the prepared statement setter
* @param rowMapper row mapper
* @return the result set
*/
@Override
public <T> SimpleResultSet<T> query(final String sql, final PreparedStatementSetter preparedStatementSetter,
final RowMapper<T> rowMapper) {
LOG.finest(() -> "Executing query '" + sql + "'...");
Expand All @@ -108,13 +80,7 @@ private PreparedStatement wrap(final PreparedStatement preparedStatement) {
return new ConvertingPreparedStatement(preparedStatement, paramSetterProvider);
}

/**
* Create a batch insert builder
*
* @param rowType row type
* @param <T> row type
* @return batch insert builder
*/
@Override
public <T> BatchInsertBuilder<T> batchInsert(final Class<T> rowType) {
return new BatchInsertBuilder<>(this::prepareStatement);
}
Expand All @@ -127,13 +93,28 @@ private PreparedStatement prepare(final String sql) {
}
}

/**
* Database dialect of this connection.
*
* @return dialect
*/
public DbDialect getDialect() {
return dialect;
void setAutoCommit(final boolean autoCommit) {
try {
this.connection.setAutoCommit(autoCommit);
} catch (final SQLException e) {
throw new UncheckedSQLException("Failed to set autoCommit to " + autoCommit, e);
}
}

void rollback() {
try {
this.connection.rollback();
} catch (final SQLException e) {
throw new UncheckedSQLException("Failed to rollback transaction", e);
}
}

void commit() {
try {
this.connection.commit();
} catch (final SQLException e) {
throw new UncheckedSQLException("Failed to commit transaction", e);
}
}

@Override
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/org/itsallcode/jdbc/SimplePreparedStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,27 @@ class SimplePreparedStatement implements AutoCloseable {
}

<T> SimpleResultSet<T> executeQuery(final ContextRowMapper<T> rowMapper) {
final ResultSet resultSet = doExecute();
final ResultSet resultSet = doExecuteQuery();
final ResultSet convertingResultSet = ConvertingResultSet.create(dialect, resultSet);
return new SimpleResultSet<>(context, convertingResultSet, rowMapper);
}

private ResultSet doExecute() {
private ResultSet doExecuteQuery() {
try {
return statement.executeQuery();
} catch (final SQLException e) {
throw new UncheckedSQLException("Error executing query '" + sql + "'", e);
}
}

boolean execute() {
try {
return statement.execute();
} catch (final SQLException e) {
throw new UncheckedSQLException("Error executing statement '" + sql + "'", e);
}
}

@Override
public void close() {
try {
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/org/itsallcode/jdbc/Transaction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.itsallcode.jdbc;

import org.itsallcode.jdbc.resultset.RowMapper;
import org.itsallcode.jdbc.resultset.SimpleResultSet;
import org.itsallcode.jdbc.resultset.generic.Row;

/**
* A running database transaction. The transaction will be rolled back
* automatically in {@link #close()}.
*/
public final class Transaction implements DbOperations {

private final SimpleConnection connection;

private Transaction(final SimpleConnection connection) {
this.connection = connection;
}

static Transaction start(final SimpleConnection connection) {
connection.setAutoCommit(false);
return new Transaction(connection);
}

/**
* Commit the transaction.
*/
public void commit() {
connection.commit();
}

/**
* Rollback the transaction.
*/
public void rollback() {
connection.rollback();
}

@Override
public void executeStatement(final String sql) {
connection.executeStatement(sql);
}

@Override
public void executeStatement(final String sql, final PreparedStatementSetter preparedStatementSetter) {
connection.executeStatement(sql, preparedStatementSetter);
}

@Override
public SimpleResultSet<Row> query(final String sql) {
return connection.query(sql);
}

@Override
public void executeScript(final String sqlScript) {
connection.executeScript(sqlScript);
}

@Override
public <T> SimpleResultSet<T> query(final String sql, final RowMapper<T> rowMapper) {
return connection.query(sql, rowMapper);
}

@Override
public <T> SimpleResultSet<T> query(final String sql, final PreparedStatementSetter preparedStatementSetter,
final RowMapper<T> rowMapper) {
return connection.query(sql, preparedStatementSetter, rowMapper);
}

@Override
public <T> BatchInsertBuilder<T> batchInsert(final Class<T> rowType) {
return connection.batchInsert(rowType);
}

/**
* Rollback transaction and enable auto commit.
* <p>
* Explicitly run {@link #commit()} before to commit your transaction.
*/
@Override
public void close() {
this.rollback();
connection.setAutoCommit(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public Iterator<T> iterator() {
* @return a list with all rows.
*/
public List<T> toList() {
return stream().toList();
try (Stream<T> stream = stream()) {
return stream.toList();
}
}

/**
Expand Down Expand Up @@ -106,7 +108,7 @@ private ResultSetIterator(final Context context, final SimpleResultSet<T> simple
this.hasNext = hasNext;
}

public static <T> Iterator<T> create(final Context context, final SimpleResultSet<T> simpleResultSet,
private static <T> Iterator<T> create(final Context context, final SimpleResultSet<T> simpleResultSet,
final ContextRowMapper<T> rowMapper) {
final boolean firstRowExists = simpleResultSet.next();
return new ResultSetIterator<>(context, simpleResultSet, rowMapper, firstRowExists);
Expand Down
Loading