/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jdbi.v3.core; import static java.util.Objects.requireNonNull; import java.io.Closeable; import java.sql.Connection; import java.sql.SQLException; import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.core.config.Configurable; import org.jdbi.v3.core.extension.ExtensionMethod; import org.jdbi.v3.core.extension.Extensions; import org.jdbi.v3.core.extension.NoSuchExtensionException; import org.jdbi.v3.core.statement.Batch; import org.jdbi.v3.core.statement.Call; import org.jdbi.v3.core.statement.PreparedBatch; import org.jdbi.v3.core.statement.Query; import org.jdbi.v3.core.statement.Script; import org.jdbi.v3.core.statement.StatementBuilder; import org.jdbi.v3.core.statement.Update; import org.jdbi.v3.core.transaction.TransactionException; import org.jdbi.v3.core.transaction.TransactionHandler; import org.jdbi.v3.core.transaction.TransactionIsolationLevel; import org.jdbi.v3.core.transaction.UnableToManipulateTransactionIsolationLevelException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This represents a connection to the database system. It usually is a wrapper around * a JDBC Connection object. */ public class Handle implements Closeable, Configurable<Handle> { private static final Logger LOG = LoggerFactory.getLogger(Handle.class); private final TransactionHandler transactions; private final Connection connection; private ThreadLocal<ConfigRegistry> config; private ThreadLocal<ExtensionMethod> extensionMethod; private StatementBuilder statementBuilder; private boolean closed = false; Handle(ConfigRegistry config, TransactionHandler transactions, StatementBuilder statementBuilder, Connection connection) { this.transactions = transactions; this.connection = connection; this.config = ThreadLocal.withInitial(() -> config); this.extensionMethod = new ThreadLocal<>(); this.statementBuilder = statementBuilder; } @Override public ConfigRegistry getConfig() { return config.get(); } void setConfig(ConfigRegistry config) { this.config.set(config); } void setConfigThreadLocal(ThreadLocal<ConfigRegistry> config) { this.config = config; } /** * Get the JDBC Connection this Handle uses. * * @return the JDBC Connection this Handle uses */ public Connection getConnection() { return this.connection; } public StatementBuilder getStatementBuilder() { return statementBuilder; } /** * Specify the statement builder to use for this handle. * @param builder StatementBuilder to be used * @return this */ public Handle setStatementBuilder(StatementBuilder builder) { this.statementBuilder = builder; return this; } /** * Closes the handle, its connection, and any other database resources it is holding. * * @throws CloseException if any resources throw exception while closing * @throws TransactionException if called while the handle has a transaction open. The open transaction will be * rolled back. */ @Override public void close() { extensionMethod.remove(); if (!closed) { boolean wasInTransaction = isInTransaction(); if (wasInTransaction) { rollback(); } try { statementBuilder.close(getConnection()); } finally { try { connection.close(); if (wasInTransaction) { throw new TransactionException("Improper transaction handling detected: A Handle with an open " + "transaction was closed. Transactions must be explicitly committed or rolled back " + "before closing the Handle. " + "Jdbi has rolled back this transaction automatically."); } } catch (SQLException e) { throw new CloseException("Unable to close Connection", e); } finally { LOG.trace("Handle [{}] released", this); closed = true; } } } } /** * @return whether the Handle is closed */ public boolean isClosed() { return closed; } /** * Convenience method which creates a query with the given positional arguments * @param sql SQL or named statement * @param args arguments to bind positionally * @return query object */ public Query select(String sql, Object... args) { Query query = this.createQuery(sql); int position = 0; for (Object arg : args) { query.bind(position++, arg); } return query; } /** * Execute a SQL statement, and return the number of rows affected by the statement. * * @param sql the SQL statement to execute, using positional parameters (if any) * @param args positional arguments * * @return the number of rows affected */ public int execute(String sql, Object... args) { Update stmt = createUpdate(sql); int position = 0; for (Object arg : args) { stmt.bind(position++, arg); } return stmt.execute(); } /** * Create a non-prepared (no bound parameters, but different SQL) batch statement. * @return empty batch * @see Handle#prepareBatch(String) */ public Batch createBatch() { return new Batch(this); } /** * Prepare a batch to execute. This is for efficiently executing more than one * of the same statements with different parameters bound. * * @param sql the batch SQL * @return a batch which can have "statements" added */ public PreparedBatch prepareBatch(String sql) { return new PreparedBatch(this, sql); } /** * Create a call to a stored procedure * * @param sql the stored procedure sql * * @return the Call */ public Call createCall(String sql) { return new Call(this, sql); } /** * Return a default Query instance which can be executed later, as long as this handle remains open. * @param sql the select sql * * @return the Query */ public Query createQuery(String sql) { return new Query(this, sql); } /** * Creates a Script from the given SQL script * * @param sql the SQL script * * @return the created Script. */ public Script createScript(String sql) { return new Script(this, sql); } /** * Create an Insert or Update statement which returns the number of rows modified. * * @param sql The statement sql * * @return the Update */ public Update createUpdate(String sql) { return new Update(this, sql); } /** * @return whether the handle is in a transaction. Delegates to the underlying * {@link TransactionHandler}. */ public boolean isInTransaction() { return transactions.isInTransaction(this); } /** * Start a transaction. * * @return the same handle */ public Handle begin() { transactions.begin(this); LOG.trace("Handle [{}] begin transaction", this); return this; } /** * Commit a transaction. * * @return the same handle */ public Handle commit() { final long start = System.nanoTime(); transactions.commit(this); LOG.trace("Handle [{}] commit transaction in {}ms", this, (System.nanoTime() - start) / 1000000L); return this; } /** * Rollback a transaction. * * @return the same handle */ public Handle rollback() { final long start = System.nanoTime(); transactions.rollback(this); LOG.trace("Handle [{}] rollback transaction in {}ms", this, ((System.nanoTime() - start) / 1000000L)); return this; } /** * Rollback a transaction to a named savepoint. * * @param savepointName the name of the savepoint, previously declared with {@link Handle#savepoint} * * @return the same handle */ public Handle rollbackToSavepoint(String savepointName) { final long start = System.nanoTime(); transactions.rollbackToSavepoint(this, savepointName); LOG.trace("Handle [{}] rollback to savepoint \"{}\" in {}ms", this, savepointName, ((System.nanoTime() - start) / 1000000L)); return this; } /** * Create a transaction savepoint with the name provided. * * @param name The name of the savepoint * @return The same handle */ public Handle savepoint(String name) { transactions.savepoint(this, name); LOG.trace("Handle [{}] savepoint \"{}\"", this, name); return this; } /** * Release a previously created savepoint. * * @param savepointName the name of the savepoint to release * @return the same handle */ public Handle release(String savepointName) { transactions.releaseSavepoint(this, savepointName); LOG.trace("Handle [{}] release savepoint \"{}\"", this, savepointName); return this; } /** * @see Connection#isReadOnly() * @return whether the connection is in read-only mode */ public boolean isReadOnly() { try { return connection.isReadOnly(); } catch (SQLException e) { throw new UnableToManipulateTransactionIsolationLevelException("Could not getReadOnly", e); } } /** * Set the Handle readOnly. * This acts as a hint to the database to improve performance or concurrency. * * May not be called in an active transaction! * * @see Connection#setReadOnly(boolean) * @param readOnly whether the Handle is readOnly */ public Handle setReadOnly(boolean readOnly) { try { connection.setReadOnly(readOnly); } catch (SQLException e) { throw new UnableToManipulateTransactionIsolationLevelException("Could not setReadOnly", e); } return this; } /** * Executes <code>callback</code> in a transaction, and returns the result of the callback. * * @param callback a callback which will receive an open handle, in a transaction. * @param <R> type returned by callback * @param <X> exception type thrown by the callback, if any * * @return value returned from the callback * * @throws X any exception thrown by the callback */ public <R, X extends Exception> R inTransaction(HandleCallback<R, X> callback) throws X { return transactions.inTransaction(this, callback); } /** * Executes <code>callback</code> in a transaction. * * @param callback a callback which will receive an open handle, in a transaction. * @param <X> exception type thrown by the callback, if any * * @throws X any exception thrown by the callback */ public <X extends Exception> void useTransaction(final HandleConsumer<X> callback) throws X { transactions.inTransaction(this, handle -> { callback.useHandle(handle); return null; }); } /** * Executes <code>callback</code> in a transaction, and returns the result of the callback. * * <p> * This form accepts a transaction isolation level which will be applied to the connection * for the scope of this transaction, after which the original isolation level will be restored. * </p> * @param level the transaction isolation level which will be applied to the connection for the scope of this * transaction, after which the original isolation level will be restored. * @param callback a callback which will receive an open handle, in a transaction. * @param <R> type returned by callback * @param <X> exception type thrown by the callback, if any * * @return value returned from the callback * * @throws X any exception thrown by the callback */ public <R, X extends Exception> R inTransaction(TransactionIsolationLevel level, HandleCallback<R, X> callback) throws X { try (TransactionResetter tr = new TransactionResetter(getTransactionIsolationLevel())) { setTransactionIsolation(level); return transactions.inTransaction(this, level, callback); } } private class TransactionResetter implements Closeable { private final TransactionIsolationLevel initial; TransactionResetter(TransactionIsolationLevel initial) { this.initial = initial; } @Override public void close() { setTransactionIsolation(initial); } } /** * Executes <code>callback</code> in a transaction. * * <p> * This form accepts a transaction isolation level which will be applied to the connection * for the scope of this transaction, after which the original isolation level will be restored. * </p> * @param level the transaction isolation level which will be applied to the connection for the scope of this * transaction, after which the original isolation level will be restored. * @param callback a callback which will receive an open handle, in a transaction. * @param <X> exception type thrown by the callback, if any * @throws X any exception thrown by the callback */ public <X extends Exception> void useTransaction(TransactionIsolationLevel level, HandleConsumer<X> callback) throws X { inTransaction(level, handle -> { callback.useHandle(handle); return null; }); } /** * Set the transaction isolation level on the underlying connection. * * @param level the isolation level to use */ public void setTransactionIsolation(TransactionIsolationLevel level) { setTransactionIsolation(level.intValue()); } /** * Set the transaction isolation level on the underlying connection. * * @param level the isolation level to use */ public void setTransactionIsolation(int level) { try { if (connection.getTransactionIsolation() == level) { // already set, noop return; } connection.setTransactionIsolation(level); } catch (SQLException e) { throw new UnableToManipulateTransactionIsolationLevelException(level, e); } } /** * Obtain the current transaction isolation level. * * @return the current isolation level on the underlying connection */ public TransactionIsolationLevel getTransactionIsolationLevel() { try { return TransactionIsolationLevel.valueOf(connection.getTransactionIsolation()); } catch (SQLException e) { throw new UnableToManipulateTransactionIsolationLevelException("unable to access current setting", e); } } /** * Create a Jdbi extension object of the specified type bound to this handle. The returned extension's lifecycle is * coupled to the lifecycle of this handle. Closing the handle will render the extension unusable. * * @param extensionType the extension class * @param <T> the extension type * @return the new extension object bound to this handle */ public <T> T attach(Class<T> extensionType) { return getConfig(Extensions.class) .findFor(extensionType, ConstantHandleSupplier.of(this)) .orElseThrow(() -> new NoSuchExtensionException("Extension not found: " + extensionType)); } public ExtensionMethod getExtensionMethod() { return extensionMethod.get(); } void setExtensionMethod(ExtensionMethod extensionMethod) { this.extensionMethod.set(extensionMethod); } /* package private */ void setExtensionMethodThreadLocal(ThreadLocal<ExtensionMethod> extensionMethod) { this.extensionMethod = requireNonNull(extensionMethod); } }