All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.dbms.StatementWrapper Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.tentackle.dbms;

import org.tentackle.log.Logger;
import org.tentackle.session.ConstraintException;
import org.tentackle.session.PersistenceException;

import java.lang.ref.WeakReference;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * A wrapper for sql statements.
* Will catch and report SQLExceptions and * keep track of being used only once after {@link Db#createStatement()}. * * @author harald */ public class StatementWrapper implements AutoCloseable { private static final Logger LOGGER = Logger.get(StatementWrapper.class); /** the managed connection. */ protected final ManagedConnection con; /** the sql statement, null if closed. */ protected Statement stmt; /** the SQL string. */ protected String sql; /** flag if statement is marked ready for being consumed. */ protected boolean ready; /** flag if statement is currently running. */ protected boolean running; /** flag if statement has been cancelled. */ protected boolean cancelled; /** flag to tell that parallel execution should not be logged. */ protected boolean parallelOk; /** pending open result set. */ protected WeakReference openResultSet; /** * Creates a wrapper for a sql statement. * * @param con the connection * @param stmt the sql statement */ public StatementWrapper (ManagedConnection con, Statement stmt) { this.con = con; this.stmt = stmt; } /** * Gets the connection. * * @return the connection */ public ManagedConnection getConnection() { return con; } /** * Gets the wrapped statement. * * @return the statement, null if closed */ public Statement getStatement () { return stmt; } /** * Returns whether parallel execution on same connection should be logged. * * @see ManagedConnection#getLogLevelForParallelOpenResultSets() * @return true if parallel is select ok */ public boolean isParallelOk() { return parallelOk; } /** * Sets whether parallel execution on same connection should be logged.
* Multiple open result sets on a single connection usually means, that * other selects are submitted while processing the result set. Under rare conditions * this might be ok, but usually it's not. * * @param parallelOk true if parallel select is ok */ public void setParallelOk(boolean parallelOk) { this.parallelOk = parallelOk; } /** * Gets the currently attached session. * * @return the db, null if statement not attached */ public Db getSession() { Db db = con.getSession(); if (db == null) { LOGGER.warning("statement " + this + " not attached to connection " + con); } return db; } /** * Same as {@link #getSession()} but nullsafe.
* Throws PersistenceException if statement no more attached to connection. * * @return the session, never null */ public Db getAttachedSession() { Db db = getSession(); if (db == null) { throw new PersistenceException("connection " + con + " already detached from statement " + this); } return db; } /** * Marks the statement to be ready for being consumed by a {@link Db} attached * to a {@link ConnectionManager}.
* This is an additional measure to enforce the programming rule that * a statement is being used only once after {@link Db#createStatement()} * (for non-prepared statements) or after {@link Db#getPreparedStatement} * for prepared statements.

* If a statement is marked ready more than once, i.e. an open result * exists (which would be closed according to the JDBC specs), a PersistenceException is thrown. * The specs in {@link java.sql.Statement} say: *

   * By default, only one ResultSet object per Statement object can be open at the same time.
   * Therefore, if the reading of one ResultSet object is interleaved with the reading of another,
   * each must have been generated by different Statement objects.
   * All execution methods in the Statement interface implicitly close a statement's current
   * ResultSet object if an open one exists.
   * 
* Without this additional measure a "ResultSet closed" exception will be thrown by the * JDBC-driver on the next usage of the first result set, and you wouldn't have any clue which * result set forced the closing. */ public synchronized void markReady() { if (isMarkedReady()) { String msg = "statement " + this + " marked ready and not consumed yet"; forceDetached(); throw new PersistenceException(getSession(), msg); } ready = true; cancelled = false; } /** * Un-marks (consumes) this statement previously marked ready. */ public synchronized void unmarkReady() { if (!isClosed() && !isMarkedReady()) { String msg = "statement " + this + " already consumed"; forceDetached(); throw new PersistenceException(getSession(), msg); } ready = false; setRunning(false); } /** * Returns whether this statement is marked ready.
* Used to determine unused pending statements in servers when closing * dead sessions. * @return true if pending */ public synchronized boolean isMarkedReady() { return ready; } /** * Consume a statement without executing it.
* The method is invoked if a rollback is performed prior to consuming the statement. */ public void consume() { LOGGER.fine("statement {0} consumed", this); unmarkReady(); // check for being marked ready and mark consumed detachSession(); } /** * Gets the sql string. * * @return the sql string */ public String getSql() { return sql; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("{ "); if (sql != null) { buf.append(sql); } else if (stmt != null) { buf.append(stmt); } else { return ""; } buf.append(" }"); if (stmt == null) { buf.append(" (closed)"); } return buf.toString(); } /** * Detach the session from the connection.
* Statements detach the session on executeUpdate or * on close() in the {@link ResultSetWrapper} after executeQuery. */ protected void detachSession() { Db db = getSession(); if (db != null) { // if not already cleaned up db.detach(); } } /** * Implementation of executeUpdate.
* * @param sql the sql string * @return the number of affected rows * @throws SQLException if update failed */ protected int executeUpdateImpl(String sql) throws SQLException { this.sql = sql; return stmt.executeUpdate(sql); } /** * Executes the given SQL statement. * * @param sql an sql-statement * @return the row count */ public int executeUpdate(String sql) { if (sql != null) { getSession().executeBatch(); // not a batchable statement -> flush batch } LOGGER.finer("execute update {0} = {1}", this, sql); try { con.countForClearWarnings(); assertOpen(); assertNotReadOnly(); unmarkReady(); // check for being marked ready and mark it consumed again Db db = getAttachedSession(); db.setAlive(true); setRunning(true); StatementHistory history = con.logStatementHistory(this, sql); int count = executeUpdateImpl(sql); history.end(); return count; } catch (SQLException e) { Db db = getAttachedSession(); boolean dead = con.checkForDeadLink(e); if (!dead && db.getBackend().isConstraintException(e)) { throw new ConstraintException(db, this.toString(), e); } throw con.createFromSqlException(this.toString(), e); } finally { setRunning(false); detachSession(); } } /** * Adds the given SQL command to the current list of commands. * * @param sql the SQL statement */ public void addBatch(String sql) { LOGGER.finer("add batch {0} = {1}", this, sql); try { assertOpen(); if (this.sql == null) { this.sql = sql; } else { this.sql += "\n" + sql; } stmt.addBatch(sql); } catch (SQLException e) { throw con.createFromSqlException(this.toString(), e); } } /** * Empties this {@code Statement} object's current list of batched SQL commands. */ public void clearBatch() { LOGGER.finer("clear batch {0}", this); try { assertOpen(); sql = null; stmt.clearBatch(); } catch (SQLException e) { throw con.createFromSqlException(this.toString(), e); } } /** * Submits a batch of commands. * * @param finish true if this is the last invocation * @return an array of update counts containing one element for each command in the batch */ public int[] executeBatch(boolean finish) { LOGGER.finer("execute batch {0} = {1}", this, sql); try { con.countForClearWarnings(); assertOpen(); assertNotReadOnly(); unmarkReady(); // check for being marked ready and mark it consumed again Db db = getAttachedSession(); db.setAlive(true); setRunning(true); StatementHistory history = con.logStatementHistory(this, sql); int[] count = stmt.executeBatch(); history.end(); return count; } catch (SQLException e) { Db db = getAttachedSession(); boolean dead = con.checkForDeadLink(e); if (!dead && db.getBackend().isConstraintException(e)) { throw new ConstraintException(db, this.toString(), e); } throw con.createFromSqlException(this.toString(), e); } finally { setRunning(false); if (finish) { detachSession(); } else { markReady(); } } } /** * Implementation of executeQuery.
* * @param sql the sql string * @return the result set * @throws SQLException if query failed */ protected ResultSet executeQueryImpl(String sql) throws SQLException { this.sql = sql; return stmt.executeQuery(sql); } /** * Creates a result set wrapper from a result set. * * @param history the statement history * @param jdbcResultSet the low-level jdbc result set * @return the wrapper */ ResultSetWrapper createResultSetWrapper(StatementHistory history, ResultSet jdbcResultSet) { ResultSetWrapper rs = new ResultSetWrapper(this, history, jdbcResultSet); con.addResultSet(rs); openResultSet = new WeakReference<>(rs); return rs; } /** * Forgets about the given result set. * * @param rs the result set wrapper */ void forgetResultSetWrapper(ResultSetWrapper rs) { con.removeResultSet(rs); openResultSet = null; } /** * Executes a query. * * @param sql is the query sql string * @param withinTx is true if start a transaction for this query. * * @return the result set as a ResultSetWrapper */ public ResultSetWrapper executeQuery (String sql, boolean withinTx) { getSession().executeBatch(); // not a batchable statement -> flush batch LOGGER.finer("execute query {0} = {1}", this, sql); long txVoucher = 0; try { con.countForClearWarnings(); assertOpen(); Db db = getAttachedSession(); db.setAlive(true); setRunning(true); // will be cleared when the result set is closed if (withinTx) { txVoucher = db.begin("executeQuery"); // returns true if autocommit on, i.e. we were not within a tx and begin() started a new one. } StatementHistory history = con.logStatementHistory(this, sql); ResultSetWrapper resultSet = createResultSetWrapper(history, executeQueryImpl(sql)); history.end(); if (withinTx) { resultSet.setCommitOnCloseVoucher(true, txVoucher); // commit tx on closing the resultset } return resultSet; } catch (SQLException e) { Db db = getAttachedSession(); boolean dead = con.checkForDeadLink(e); if (withinTx && !dead) { try { db.rollback(txVoucher); } catch (RuntimeException rex) { // just log LOGGER.severe("statement failed", e); } } else { // dead or not within a transaction: application cannot roll back -> force cleanup try { db.forceDetached(); } catch (RuntimeException rex) { // just log LOGGER.severe("detach failed", e); } } throw con.createFromSqlException(this.toString(), e); } } /** * Executes a query. * * @param sql is the query sql string * @return the result set as a ResultSetWrapper */ public ResultSetWrapper executeQuery (String sql) { return executeQuery(sql, false); } /** * Sets the running flag of this statements.
* Updates the running statements in {@link ManagedConnection}. * * @param running true if running, false if finished */ protected synchronized void setRunning(boolean running) { this.running = running; if (running) { con.addRunningStatement(this); } else { con.removeRunningStatement(this); } } /** * Returns whether the statement is being executed.
* May be invoked from any thread. * * @return true if statement is running */ public synchronized boolean isRunning() { return running; } /** * Cancels a statement. */ public synchronized void cancel() { try { if (stmt != null) { LOGGER.info("canceling statement: {0}", this); stmt.cancel(); } setRunning(false); cancelled = true; } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Returns whether this statement has been cancelled.
* May be invoked from any thread. * * @return true if cancelled */ public synchronized boolean isCancelled() { return cancelled; } /** * Closes this statement. */ @Override public void close () { final Statement st = stmt; // local copy in case of race-condition if (st != null) { try { if (isMarkedReady()) { LOGGER.warning("statement " + this + " not consumed -> cleanup"); ready = false; } st.close(); // closing an already closed statement is ok } catch (SQLException sqx) { throw new PersistenceException(getSession(), toString(), sqx); } finally { stmt = null; // this marks it closed! final WeakReference ref = openResultSet; // local copy in case of race-condition if (ref != null) { ResultSetWrapper rs = ref.get(); if (rs != null) { try { rs.close(); } catch (RuntimeException rex) { LOGGER.severe("closing result set failed for statement " + this, rex); } } openResultSet = null; } } } } /** * Determines whether this statement is closed. * * @return true if statement closed */ public boolean isClosed() { return stmt == null; } /** * Gives the JDBC driver a hint as to the number of rows that should * be fetched from the database when more rows are needed for * ResultSet objects generated by this Statement. * If the value specified is zero, then the hint is ignored. * The default value is zero. * * @param rows the number of rows to fetch * @see #getFetchSize */ public void setFetchSize(int rows) { assertOpen(); try { stmt.setFetchSize(rows); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Retrieves the number of result set rows that is the default * fetch size for ResultSet objects * generated from this Statement object. * If this Statement object has not set * a fetch size by calling the method setFetchSize, * the return value is implementation-specific. * * @return the default fetch size for result sets generated * from this Statement object * @see #setFetchSize */ public int getFetchSize() { assertOpen(); try { return stmt.getFetchSize(); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Sets the limit for the maximum number of rows that any * ResultSet object generated by this Statement * object can contain to the given number. * If the limit is exceeded, the excess * rows are silently dropped. * * @param max the new max rows limit; zero means there is no limit * @see #getMaxRows */ public void setMaxRows(int max) { assertOpen(); try { stmt.setMaxRows(max); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Retrieves the maximum number of rows that a * ResultSet object produced by this * Statement object can contain. If this limit is exceeded, * the excess rows are silently dropped. * * @return the current maximum number of rows for a ResultSet * object produced by this Statement object; * zero means there is no limit * @see #setMaxRows */ public int getMaxRows() { assertOpen(); try { return stmt.getMaxRows(); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Gives the driver a hint as to the direction in which * rows will be processed in ResultSet * objects created using this Statement object. The * default value is ResultSet.FETCH_FORWARD. *

* Note that this method sets the default fetch direction for * result sets generated by this Statement object. * Each result set has its own methods for getting and setting * its own fetch direction. * * @param direction the initial direction for processing rows * @see #getFetchDirection */ public void setFetchDirection(int direction) { assertOpen(); try { stmt.setFetchDirection(direction); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Retrieves the direction for fetching rows from * database tables that is the default for result sets * generated from this Statement object. * If this Statement object has not set * a fetch direction by calling the method setFetchDirection, * the return value is implementation-specific. * * @return the default fetch direction for result sets generated * from this Statement object * @see #setFetchDirection */ public int getFetchDirection() { assertOpen(); try { return stmt.getFetchDirection(); } catch (SQLException e) { throw new PersistenceException(getSession(), this.toString(), e); } } /** * Asserts the statement is open. */ protected void assertOpen() { if (stmt == null) { throw new PersistenceException(getSession(), "statement already closed"); } } /** * Asserts db is writable. */ protected void assertNotReadOnly() { Db db = getSession(); if (db != null && db.isReadOnly()) { throw new PersistenceException(db, "is read-only"); } } /** * Detaches the Db after a severe error. *

* Invoked to clean up any pending statements, rollback the transaction (if any) * and detach the db after a severe error. * This is a cleanup measure in case the application does not handle * exceptions properly. */ protected void forceDetached() { try { Db db = getSession(); if (db != null) { db.forceDetached(); } } catch (RuntimeException rex) { LOGGER.severe("emergency detach failed", rex); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy