com.databasesandlife.util.jdbc.DbTransaction Maven / Gradle / Ivy
Show all versions of java-common Show documentation
package com.databasesandlife.util.jdbc;
import com.databasesandlife.util.Timer;
import com.databasesandlife.util.YearMonthDay;
import com.microsoft.sqlserver.jdbc.SQLServerDriver;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TableRecord;
import org.jooq.impl.DSL;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static com.databasesandlife.util.gwtsafe.ConfigurationException.prefixExceptionMessage;
/**
* Represents a transaction against a database.
*
* MySQL, PostgreSQL and SQL Server and SQLite are supported.
*
* An object is created with the JDBC URL to the database.
* There is no factory for this type of object, simply store the string JDBC URL as opposed to a DbConnectionFactory.
*
* The following facilities are provided
* (i.e. these are the reasons why objects of this class might be preferred over simply using JDBC directly):
*
* - SQLExceptions are not thrown.
* This allows checked Exceptions to be used in client code to represent things that might actually happen.
* If a SQLException occurs e.g. due to database connectivity, it is assumed there is nothing the program can do apart
* from display an error to the user.
*
- {@link #execute} method prepares statements and does parameter substitution in one line.
*
- {@link #query} method acts like execute, but returns an {@link Iterable} of objects representing rows.
* This is more convenient for the java "for" statement than the JDBC ResultSet object.
*
- Various extra data types are supported such as "points in time" stored as GMT date/times using {@link Date java.util.Date},
* {@link LocalDate}, etc.
*
- {@link #insert} and {@link #update} take Maps of columns as arguments (easier than maintaining SQL strings)
*
- {@link #insertAndFetchNewId} performs an insert and returns the new "auto-increment ID".
*
- {@link #insertIgnoringUniqueConstraintViolations} and {@link #updateIgnoringUniqueConstraintViolations}
* perform inserts and updates, but ignore any unique constraint violations.
* For example using the "insert then update" pattern, for "just-in-time" creating records, can use these methods.
*
- {@link #attempt(Runnable)} establishes a savepoint before the runnable and rolls back to it on failure,
* necessary for any operation that may fail when using PostgreSQL.
* - The transaction isolation level is set to REPEATABLE READ. (This is the default in MySQL but not other databases.)
*
- You can register {@link RollbackListener} objects with {@link #addRollbackListener(RollbackListener)}.
* When the transaction rolls back, this listener will get called.
* This is so that any primary keys which have been assigned and stored in Java objects,
* which are now no longer valid due to the rollback, may be removed from the Java objects.
*
*
* Upon creating an object, a connection is made to the database, and a transaction is started.
* Upon executing {@link #commit()} or {@link #rollback()} the connection is closed.
* Although opening a connection each time is not as efficient as using a connection pool, this class is extremely simple,
* which has advantages both in terms or reliability, maintainability and also speed. (For example, C3P0 has > 50 KLOC).
* Opening a connection to MySQL is fast.
*
*
* DbTransaction objects are not thread safe; do not use them from multiple threads simultaneously.
*
* Example usage:
*
* String jdbc = "jdbc:mysql://hostname/dbName?user=x&password=x&useUnicode=true&characterEncoding=UTF-8";
* try (DbTransaction db = new DbTransaction(jdbc)) {
* db.execute("DELETE FROM x WHERE id=?", 9);
* db.commit();
* }
*
*
* @author This source is copyright Adrian Smith and licensed under the LGPL 3.
* @see Project on GitHub
*/
@SuppressWarnings({ "deprecation", "UseOfObsoleteDateTimeApi" })
public class DbTransaction implements DbQueryable, AutoCloseable {
public final DbServerProduct product;
protected Connection connection; // null means already committed
protected final List rollbackListeners = new ArrayList<>();
protected final Map preparedStatements = new HashMap<>();
protected final Map>, String> postgresTypeForEnum = new HashMap<>();
@Override
public void close() {
rollbackIfConnectionStillOpen();
}
public enum DbServerProduct { mysql, postgres, sqlserver, sqlite };
@FunctionalInterface
public interface DbTransactionFactory {
/** Caller must call {@link DbTransaction#commit()} or {@link DbTransaction#rollback()}. */
public DbTransaction newDbTransaction();
}
public static class UniqueConstraintViolation extends Exception {
public final String constraintName;
public UniqueConstraintViolation(String c) { super(); constraintName=c; }
public UniqueConstraintViolation(String c, Throwable t) { super(t); constraintName=c; }
}
public static class ForeignKeyConstraintViolation extends Exception {
public ForeignKeyConstraintViolation() { super(); }
public ForeignKeyConstraintViolation(Throwable t) { super(t); }
}
public static class SqlException extends RuntimeException {
public SqlException(Throwable x) { super(x); }
public SqlException(String x) { super(x); }
public SqlException(String prefix, Throwable t) { super(prefixExceptionMessage(prefix, t), t); }
}
public static class CannotConnectToDatabaseException extends RuntimeException {
public CannotConnectToDatabaseException(String x) { super(x); }
public CannotConnectToDatabaseException(String x, Throwable t) { super(x, t); }
}
@FunctionalInterface public interface RollbackListener {
public void transactionHasRolledback();
}
public static class DbQueryResultRow {
ResultSet rs;
DbQueryResultRow(ResultSet rs) { this.rs = rs; }
public boolean hasColumn(String columnName) {
try {
var rsmd = rs.getMetaData();
var columnCount = rsmd.getColumnCount();
for (var x = 1; x <= columnCount; x++)
if (columnName.equals(rsmd.getColumnName(x))) return true;
return false;
}
catch (SQLException e) { throw new RuntimeException(e); }
}
public List getColumnNames() {
try {
var rsmd = rs.getMetaData();
var columnCount = rsmd.getColumnCount();
var result = new ArrayList(columnCount);
for (var i = 1; i <= columnCount; i++) result.add(rsmd.getColumnName(i));
return result;
}
catch (SQLException e) { throw new RuntimeException(e); }
}
@SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL") // We want to return null here, this is by design
public Boolean getBoolean(String col){
try { boolean result = rs.getBoolean(col); if (rs.wasNull()) return null; else return result; }
catch (SQLException e) { throw new RuntimeException(e); }
}
public String getString(String col) {
try { return rs.getString(col); }
catch (SQLException e) { throw new RuntimeException(e); }
}
public Integer getInt(String col) {
try { int result = rs.getInt(col); if (rs.wasNull()) return null; else return result; }
catch (SQLException e) { throw new RuntimeException(e); }
}
public Long getLong(String col) {
try { long result = rs.getLong(col); if (rs.wasNull()) return null; else return result; }
catch (SQLException e) { throw new RuntimeException(e); }
}
public Double getDouble(String col) {
try { double result = rs.getDouble(col); if (rs.wasNull()) return null; else return result; }
catch (SQLException e) { throw new RuntimeException(e); }
}
/**
* for the bytea/blob data type - returns the stream, does not convert to byte[]
* @param col column name
* @return InputStream
*/
public InputStream getBinaryStream(String col){
try { InputStream result = rs.getBinaryStream(col); if (rs.wasNull()) return null; else return result; }
catch (SQLException e) { throw new RuntimeException(e); }
}
/**
* for the bytea/blob data type - returns byte[]
* @param col column name
* @return byte[];
*/
public byte[] getByteArray(String col){
try {
byte[] result = rs.getBytes(col);
if (rs.wasNull())
return null;
else
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Date getDate(String col) {
try {
var str = rs.getString(col);
if (str == null) return null;
var f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
f.setTimeZone(TimeZone.getTimeZone("UTC"));
return f.parse(str);
}
catch (SQLException | ParseException e) { throw new RuntimeException(e); }
}
public String[] getStringArray(String col) {
try {
java.sql.Array x = rs.getArray(col);
if (x == null) return null;
Object[] a = (Object[]) x.getArray();
return Arrays.copyOf(a, a.length, String[].class);
}
catch (SQLException e) { throw new RuntimeException(e); }
}
public Integer[] getIntegerArray(String col) {
try {
java.sql.Array x = rs.getArray(col);
if (x == null) return null;
Object[] a = (Object[]) x.getArray();
return Arrays.copyOf(a, a.length, Integer[].class);
}
catch (SQLException e) { throw new RuntimeException(e); }
}
/** Reads column as string and expects "YYYY-MM-DD" format */
public YearMonthDay getYearMonthDay(String col) {
try {
var str = rs.getString(col);
if (str == null) return null;
if (str.length() > "YYYY-MM-DD".length()) str = str.substring(0, "YYYY-MM-DD".length()); // e.g. if col is datetime
return YearMonthDay.newForYYYYMMDD(str);
}
catch (SQLException e) { throw new RuntimeException(e); }
}
public LocalDate getLocalDate(String col) {
try {
var str = rs.getString(col);
if (str == null) return null;
return LocalDate.parse(str);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public LocalTime getLocalTime(String col) {
try {
var str = rs.getString(col);
if (str == null) return null;
return LocalTime.parse(str);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
public > T getEnum(String col, Class clazz) {
try {
var str = rs.getString(col);
if (str == null) return null;
var valueOfMethod = clazz.getMethod("valueOf", String.class);
return (T) valueOfMethod.invoke(null, str);
}
catch (SQLException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/**
* The SELECT must supply a VARCHAR[] as PostgreSQL JDBC driver does not implement ENUM[].
* For example SELECT my_enum_array::VARCHAR[] ....
*/
@SuppressWarnings("unchecked")
public > T[] getEnumArray(String col, Class extends T> componentClass){
try {
java.sql.Array x = rs.getArray(col);
if (x == null) return null;
Object[] stringArrayFromDb = (Object[]) x.getArray();
T[] result = (T[]) Array.newInstance(componentClass, stringArrayFromDb.length);
var valueOfMethod = componentClass.getMethod("valueOf", String.class);
for (var i = 0; i < stringArrayFromDb.length; i++)
result[i] = (T) valueOfMethod.invoke(null, stringArrayFromDb[i]);
return result;
}
catch (SQLException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
public static class DbQueryResultRowIterator implements Iterator {
enum State { readingData, /** rs is actually one row forward of iterator */ peeked, finished };
ResultSet rs;
State state = State.readingData;
protected DbQueryResultRowIterator(ResultSet rs) {
this.rs = rs;
hasNext(); // this forces the statement to really be executed; important for timing
}
@Override public boolean hasNext() {
try {
if (state == State.readingData) { if (rs.next()) state = State.peeked; else state = State.finished; }
if (state == State.peeked) return true;
if (state == State.finished) return false;
throw new RuntimeException();
}
catch (SQLException e) { throw new RuntimeException(e); }
}
@Override public DbQueryResultRow next() {
hasNext(); // make sure we are peeking or finished
if (state == State.peeked) { state = State.readingData; return new DbQueryResultRow(rs); }
if (state == State.finished) throw new NoSuchElementException();
throw new RuntimeException();
}
@Override public void remove() {
throw new UnsupportedOperationException();
}
}
public abstract static class DbQueryResultSet implements Iterable {
public Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
/**
* Reads all rows in the result set, finds the string column "stringColumnName" and creates objects of type "cl" by
* calling its constructor taking a single string argument.
*/
public List toObjectList(Class cl, String stringColumnName) {
try {
var i = iterator();
var result = new ArrayList();
while (i.hasNext()) {
var val = i.next().getString(stringColumnName);
var obj = cl.getConstructor(String.class).newInstance(val);
result.add(obj);
}
return result;
}
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new RuntimeException(e);
}
}
/**
* Reads all rows in the result set, finds the string column "stringColumnName" and creates objects of type "cl" by
* calling its constructor taking a single string argument.
*/
public Set toObjectSet(Class cl, String stringColumnName) {
return new HashSet(toObjectList(cl, stringColumnName));
}
public Set toIntegerSet(String columnName) {
var result = new HashSet();
for (var row : this) result.add(row.getInt(columnName));
return result;
}
public Set toLongSet(String columnName) {
var result = new HashSet();
for (var row : this) result.add(row.getLong(columnName));
return result;
}
}
// ---------------------------------------------------------------------------------------------------------------
// Internal methods
// ---------------------------------------------------------------------------------------------------------------
protected void logNewTransaction() {
LoggerFactory.getLogger(DbTransaction.class.getName() + "." + "newTransaction").info("Starting new transaction...");
}
protected Connection getConnection() {
if (connection == null) throw new IllegalStateException("connection already committed or rolledback");
return connection;
}
protected PreparedStatement getPreparedStatement(String sql) throws SQLException {
var c = getConnection(); // throws if already committed/rolledback
var ps = (PreparedStatement) preparedStatements.get(sql);
if (ps != null) return ps;
ps = c.prepareStatement(sql);
ps.setFetchSize(50);
preparedStatements.put(sql, ps);
return ps;
}
protected PreparedStatement insertParamsToPreparedStatement(String sql, Object... args) throws SQLException {
var utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
var ps = getPreparedStatement(sql);
for (var i = 0; i < args.length; i++) {
try {
if (args[i] == null)
ps.setNull(i+1,Types.NULL);
else if (args[i] instanceof Boolean b)
ps.setBoolean(i+1, b);
else if (args[i] instanceof String s) // setXx 1st param: first arg is 1 not 0
ps.setString(i+1, s);
else if (args[i] instanceof Integer v)
ps.setInt(i+1, v);
else if (args[i] instanceof Long l)
ps.setLong(i+1, l);
else if (args[i] instanceof Double d)
ps.setDouble(i+1, d);
else if (args[i] instanceof BigDecimal d)
ps.setBigDecimal(i+1, d);
else if (args[i] instanceof java.util.Date date)
switch (product) {
case mysql:
// Can't set Timestamp object directly, see http://bugs.mysql.com/bug.php?id=15604 w.r.t GMT timezone
var f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
f.setTimeZone(TimeZone.getTimeZone("UTC"));
ps.setString(i+1, f.format(date));
break;
default:
var ts = new java.sql.Timestamp(date.getTime());
ps.setTimestamp(i+1, ts, utc);
}
else if (args[i] instanceof YearMonthDay d)
switch (product) {
case postgres:
ps.setDate(i+1, new java.sql.Date(d.getMidnightUtcAtStart().getTime()), utc);
break;
default:
ps.setString(i+1, ((YearMonthDay) args[i]).toYYYYMMDD());
}
else if (args[i] instanceof LocalTime t)
ps.setTime(i+1, java.sql.Time.valueOf(t));
else if (args[i] instanceof LocalDate d)
ps.setDate(i+1, java.sql.Date.valueOf(d));
else if (args[i] instanceof LocalDateTime t)
ps.setTimestamp(i+1, java.sql.Timestamp.valueOf(t));
else if (args[i] instanceof byte[] b)
ps.setBytes(i+1, b);
else if (args[i] instanceof Enum> e)
ps.setString(i+1, e.name());
else if (args[i] instanceof String[] arr)
ps.setArray(i+1, connection.createArrayOf("varchar", arr));
else if (args[i] instanceof Integer[] arr)
ps.setArray(i+1, connection.createArrayOf("int", arr));
else if (args[i] instanceof Long[] arr)
ps.setArray(i+1, connection.createArrayOf("int", arr));
else if (args[i] instanceof Enum>[] arr)
switch (product) {
case postgres:
ps.setArray(i+1, connection.createArrayOf(postgresTypeForEnum.get(args[i].getClass().getComponentType()), arr));
break;
default:
throw new RuntimeException("Enum Arrays are not supported for: " + product);
}
else
throw new RuntimeException("sql='"+sql+
"': unexpected type for argument "+i+": "+args[i].getClass());
}
catch (SQLException e) {
throw new RuntimeException("sql='"+sql+
"': unexpected error setting argument "+i+": "+e.getMessage(), e);
}
}
return ps;
}
protected long fetchNewPkValue() {
try {
String sql = switch (product) {
case mysql -> "SELECT LAST_INSERT_ID() AS id";
case postgres -> "SELECT lastval() AS id";
default -> throw new RuntimeException();
};
var ps = insertParamsToPreparedStatement(sql);
var rs = ps.executeQuery();
if ( ! rs.next()) throw new RuntimeException("SQL to request just-inserted PK value returned no results");
var result = rs.getLong("id");
rs.close();
return result;
}
catch (SQLException e) { throw new SqlException(e); }
}
protected String getQuestionMarkForValue(Object value) {
if (product == DbServerProduct.postgres) {
if (value instanceof Enum>) {
var type = postgresTypeForEnum.get(value.getClass());
if (type == null) throw new RuntimeException("Cannot convert Java Enum '" + value.getClass() + "' to " +
"Postgres ENUM type: use addPostgresTypeForEnum method after DbTransaction constructor");
return "?::" + type;
}
if (value instanceof Enum>[]) {
var type = postgresTypeForEnum.get(value.getClass().getComponentType());
if (type == null) throw new RuntimeException("Cannot convert Java Enum '" + value.getClass() + "' to " +
"Postgres ENUM type: use addPostgresTypeForEnum method after DbTransaction constructor");
return "?::" + type + "[]";
}
}
return "?";
}
/** @return the name of the unique constraint that was violated, or null if the error was not about a unique constraint violation */
public static String parseUniqueConstraintViolationOrNull(String msg) {
{ Matcher m = Pattern.compile("Duplicate entry '.*' for key '(.*)'").matcher(msg); if (m.find()) return m.group(1); } // MySQL
{ Matcher m = Pattern.compile("violates unique constraint \"(.*)\"").matcher(msg); if (m.find()) return m.group(1); } // PostgreSQL
{ Matcher m = Pattern.compile("verletzt Unique-Constraint „(.*)“").matcher(msg); if (m.find()) return m.group(1); } // PostgreSQL German
return null;
}
public static boolean isForeignKeyConstraintViolation(String msg) {
if (msg.contains("foreign key constraint")) return true; // MySQL, PostgreSQL
if (msg.contains("verletzt Fremdschl")) return true; // PostgreSQL German
return false;
}
/**
* If "exception" represents a violation exception it is thrown and the connection is rolled back to "initialState",
* otherwise the original exception is re-thrown.
*/
protected void rollbackToSavepointAndThrowConstraintViolation(Savepoint initialState, RuntimeException exception)
throws UniqueConstraintViolation, ForeignKeyConstraintViolation {
try {
var uniqueConstraintViolationOrNull = parseUniqueConstraintViolationOrNull(exception.getMessage());
var isForeignKeyConstraintViolation = isForeignKeyConstraintViolation(exception.getMessage());
if (uniqueConstraintViolationOrNull!=null || isForeignKeyConstraintViolation) {
if (initialState != null) {
connection.rollback(initialState);
connection.releaseSavepoint(initialState);
}
if (uniqueConstraintViolationOrNull!=null) throw new UniqueConstraintViolation(uniqueConstraintViolationOrNull, exception);
else if (isForeignKeyConstraintViolation) throw new ForeignKeyConstraintViolation(exception);
else throw new RuntimeException();
}
else {
throw exception;
}
}
catch (SQLException e) { throw new RuntimeException(e); }
}
protected void closeConnection() {
try {
for (var p : preparedStatements.values()) p.close();
connection.close();
connection = null;
}
catch (SQLException ignored) { } // ignore errors on closing
}
// ---------------------------------------------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------------------------------------------
public DbTransaction(String jdbcUrl) throws CannotConnectToDatabaseException {
try {
logNewTransaction();
if (jdbcUrl.contains(":mysql")) product = DbServerProduct.mysql;
else if (jdbcUrl.contains(":postgres")) product = DbServerProduct.postgres;
else if (jdbcUrl.contains(":sqlserver")) product = DbServerProduct.sqlserver;
else if (jdbcUrl.contains(":sqlite")) product = DbServerProduct.sqlite;
else throw new CannotConnectToDatabaseException("Unrecognized server product: " + jdbcUrl);
switch (product) { // load the classes so that getConnection recognizes the :mysql: etc part of JDBC url
case mysql: new com.mysql.jdbc.Driver(); break;
case postgres: new org.postgresql.Driver(); break;
case sqlserver: new SQLServerDriver(); break;
case sqlite: break;
default: throw new RuntimeException("Unreachable");
}
connection = DriverManager.getConnection(jdbcUrl);
connection.setAutoCommit(false);
if (product != DbServerProduct.sqlite)
execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
}
catch (SQLException e) {
throw new CannotConnectToDatabaseException("cannot connect to database '"+jdbcUrl+"': JBDC driver is OK, "+
"connection is NOT OK: "+e.getMessage(), e);
}
}
public DbTransaction(DbServerProduct product, Connection connection) {
this.product = product;
this.connection = connection;
}
public void addPostgresTypeForEnum(Class extends Enum>> enumClass, String postgresType) {
postgresTypeForEnum.put(enumClass, postgresType);
}
/**
* For example, during insert in jOOQ:
*
* db.addRollbackListener(() -> {
* venue.setVid(null);
* venue.changed(VENUES.VID, false);
* } );
*
*/
public void addRollbackListener(RollbackListener listener) {
rollbackListeners.add(listener);
}
public DSLContext jooq() {
SQLDialect d = switch (product) {
case mysql -> SQLDialect.MYSQL;
case postgres -> SQLDialect.POSTGRES;
default -> throw new RuntimeException();
};
return DSL.using(getConnection(), d);
}
public static String getSqlForLog(String sql, Object[] args) {
var result = new StringBuilder();
var m = Pattern.compile(Pattern.quote("?")).matcher(sql);
var argIdx = 0;
while (m.find()) {
var arg = args[argIdx++];
var argUnencoded = "" + arg;
if (arg instanceof java.util.Date) {
var f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
f.setTimeZone(TimeZone.getTimeZone("UTC"));
argUnencoded = f.format((java.util.Date) arg);
}
var argEncoded = argUnencoded.replace("'", "\\'");
m.appendReplacement(result, Matcher.quoteReplacement("'" + argEncoded + "'"));
}
m.appendTail(result);
return result.toString();
}
/** @return Never retuns null (but may return an empty iterable) */
public DbQueryResultSet query(final String sql, final Object... args) {
return new DbQueryResultSet() {
public Iterator iterator() {
try (var ignored = new Timer("SQL: " + getSqlForLog(sql, args))) {
var ps = insertParamsToPreparedStatement(sql, args);
var rs = ps.executeQuery();
return new DbQueryResultRowIterator(rs);
}
catch (SQLException e) { throw new SqlException(getSqlForLog(sql, args), e); }
}
};
}
/** @return Never retuns null (but may return an empty iterable) */
public DbQueryResultSet query(CharSequence sql, List> args) {
return query(sql.toString(), args.toArray());
}
public void execute(String sql, Object... args) throws SqlException {
try { insertParamsToPreparedStatement(sql, args).executeUpdate(); } // returns int = row count processed; we ignore
catch (SQLException e) { throw new SqlException("database error ("+ getSqlForLog(sql, args)+")", e); }
}
/**
* Sets a savepoint as is necessary on PostgreSQL, runs the code,
* then rolls back to the savepoint on RuntimeException or discards the savepoint on success.
*/
public void attempt(Runnable r) {
try {
var initialState = connection.setSavepoint();
try {
r.run();
}
catch (RuntimeException e) {
connection.rollback(initialState);
throw e;
}
finally {
connection.releaseSavepoint(initialState);
}
}
catch (SQLException e) { throw new SqlException(e); }
}
/** For normal delete where you don't expect a possible foreign key constraint violation, use {@link #execute(String, Object...)} instead */
public void deleteOrThrowForeignKeyConstraintViolation(String table, String where, Object... args) throws ForeignKeyConstraintViolation {
try {
Savepoint initialState = null;
if (product == DbServerProduct.postgres) initialState = connection.setSavepoint();
try {
execute("DELETE FROM " + table + " WHERE " + where, args);
if (initialState != null) connection.releaseSavepoint(initialState);
}
catch (RuntimeException e) {
rollbackToSavepointAndThrowConstraintViolation(initialState, e);
}
}
catch (UniqueConstraintViolation e) { throw new RuntimeException("Unreachable", e); }
catch (SQLException e) { throw new SqlException(e); }
}
public void execute(CharSequence sql, List> args) {
execute(sql.toString(), args.toArray());
}
protected void appendSetClauses(StringBuilder sql, List