jooq/jOOQ/src/main/java/org/jooq/impl/DefaultExecuteContext.java

728 lines
22 KiB
Java

/*
* 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.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;
import static org.jooq.conf.SettingsTools.renderLocale;
import static org.jooq.impl.Tools.EMPTY_INT;
import static org.jooq.impl.Tools.EMPTY_QUERY;
import static org.jooq.impl.Tools.EMPTY_STRING;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLOutput;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.jooq.Configuration;
import org.jooq.ConnectionProvider;
import org.jooq.Constants;
import org.jooq.ConverterScope;
import org.jooq.DDLQuery;
import org.jooq.DSLContext;
import org.jooq.Delete;
import org.jooq.ExecuteContext;
import org.jooq.ExecuteType;
import org.jooq.Insert;
import org.jooq.Merge;
import org.jooq.Query;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.ResultQuery;
import org.jooq.Routine;
import org.jooq.SQLDialect;
import org.jooq.Scope;
import org.jooq.Update;
import org.jooq.conf.Settings;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.jdbc.JDBCUtils;
import org.jetbrains.annotations.NotNull;
// ...
/**
* A default implementation for the {@link ExecuteContext}.
*
* @author Lukas Eder
*/
class DefaultExecuteContext implements ExecuteContext {
private static final JooqLogger log = JooqLogger.getLogger(DefaultExecuteContext.class);
// Persistent attributes (repeatable)
private final ConverterScope converterScope;
private final Instant creationTime;
private final Configuration originalConfiguration;
private final Configuration derivedConfiguration;
private final Map<Object, Object> data;
private final Query query;
private final Routine<?> routine;
private String sql;
private final boolean batch;
private final Query[] batchQueries;
private final String[] batchSQL;
private final int[] batchRows;
ConnectionProvider connectionProvider;
private Connection connection;
private SettingsEnabledConnection wrappedConnection;
private PreparedStatement statement;
private int statementExecutionCount;
private ResultSet resultSet;
private Record record;
private Result<?> result;
int recordLevel;
int resultLevel;
private int rows = -1;
private RuntimeException exception;
private SQLException sqlException;
private SQLWarning sqlWarning;
private String[] serverOutput;
// ------------------------------------------------------------------------
// XXX: Static utility methods for handling blob / clob lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<List<AutoCloseable>> RESOURCES = new ThreadLocal<>();
/**
* Clean up blobs, clobs and the local configuration.
* <p>
* <h5>BLOBS and CLOBS</h5>
* <p>
* [#1326] This is necessary in those dialects that have long-lived
* temporary lob objects, which can cause memory leaks in certain contexts,
* where the lobs' underlying session / connection is long-lived as well.
* Specifically, Oracle and ojdbc have some trouble when streaming temporary
* lobs to UDTs:
* <ol>
* <li>The lob cannot have a call-scoped life time with UDTs</li>
* <li>Freeing the lob after binding will cause an ORA-22275</li>
* <li>Not freeing the lob after execution will cause an
* {@link OutOfMemoryError}</li>
* </ol>
* <p>
* <h5>Local configuration</h5>
* <p>
* [#1544] There exist some corner-cases regarding the {@link SQLOutput}
* API, used for UDT serialisation / deserialisation, which have no elegant
* solutions of obtaining a {@link Configuration} and thus a JDBC
* {@link Connection} object short of:
* <ul>
* <li>Making assumptions about the JDBC driver and using proprietary API,
* e.g. that of ojdbc</li>
* <li>Dealing with this problem globally by using such a local
* configuration</li>
* </ul>
*
* @see <a
* href="http://stackoverflow.com/q/11439543/521799">http://stackoverflow.com/q/11439543/521799</a>
*/
static final void clean() {
List<AutoCloseable> resources = RESOURCES.get();
if (resources != null) {
for (AutoCloseable resource : resources)
JDBCUtils.safeClose(resource);
RESOURCES.remove();
}
LOCAL_CONNECTION.remove();
}
/**
* Register a blob for later cleanup with {@link #clean()}
*/
static final void register(Blob blob) {
register((AutoCloseable) blob::free);
}
/**
* Register a clob for later cleanup with {@link #clean()}
*/
static final void register(Clob clob) {
register((AutoCloseable) clob::free);
}
/**
* Register an xml for later cleanup with {@link #clean()}
*/
static final void register(SQLXML xml) {
register((AutoCloseable) xml::free);
}
/**
* Register an array for later cleanup with {@link #clean()}
*/
static final void register(Array array) {
register((AutoCloseable) array::free);
}
/**
* Register a closeable for later cleanup with {@link #clean()}
*/
static final void register(AutoCloseable closeable) {
List<AutoCloseable> list = RESOURCES.get();
if (list == null) {
list = new ArrayList<>();
RESOURCES.set(list);
}
list.add(closeable);
}
// ------------------------------------------------------------------------
// XXX: Static utility methods for handling Configuration lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<ExecuteContext> LOCAL_EXECUTE_CONTEXT = new ThreadLocal<>();
/**
* Get the registered {@link ExecuteContext}.
* <p>
* It can be safely assumed that such a configuration is available once the
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final ExecuteContext localExecuteContext() {
return LOCAL_EXECUTE_CONTEXT.get();
}
/**
* Run a runnable with a new {@link #localExecuteContext()}.
*/
static final <E extends Exception> void localExecuteContext(ExecuteContext ctx, ThrowingRunnable<E> runnable) throws E {
localExecuteContext(ctx, () -> { runnable.run(); return null; });
}
/**
* Run a supplier with a new {@link #localExecuteContext()}.
*/
static final <T, E extends Exception> T localExecuteContext(ExecuteContext ctx, ThrowingSupplier<T, E> supplier) throws E {
ExecuteContext old = localExecuteContext();
try {
LOCAL_EXECUTE_CONTEXT.set(ctx);
return supplier.get();
}
finally {
LOCAL_EXECUTE_CONTEXT.set(old);
}
}
// ------------------------------------------------------------------------
// XXX: Static utility methods for handling Configuration lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<Connection> LOCAL_CONNECTION = new ThreadLocal<>();
/**
* Get the registered connection.
* <p>
* It can be safely assumed that such a connection is available once the
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final Connection localConnection() {
return LOCAL_CONNECTION.get();
}
/**
* Get the registered connection's "target connection" through
* {@link Configuration#unwrapperProvider()} if applicable.
* <p>
* It can be safely assumed that such a connection is available once the
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final Connection localTargetConnection(Scope scope) {
Connection result = localConnection();
log.info("Could not unwrap native Connection type. Consider implementing an org.jooq.UnwrapperProvider");
return result;
}
// ------------------------------------------------------------------------
// XXX: Constructors
// ------------------------------------------------------------------------
DefaultExecuteContext(Configuration configuration) {
this(configuration, null, null, null);
}
DefaultExecuteContext(Configuration configuration, Query[] batchQueries) {
this(configuration, null, batchQueries, null);
}
DefaultExecuteContext(Configuration configuration, Query query) {
this(configuration, query, null, null);
}
DefaultExecuteContext(Configuration configuration, Routine<?> routine) {
this(configuration, null, null, routine);
}
private DefaultExecuteContext(Configuration configuration, Query query, Query[] batchQueries, Routine<?> routine) {
// [#4277] The ExecuteContext's Configuration will always return the same Connection,
// e.g. when running statements from sub-ExecuteContexts
// [#7569] The original configuration is attached to Record and Result instances
this.creationTime = configuration.clock().instant();
this.connectionProvider = configuration.connectionProvider();
this.originalConfiguration = configuration;
this.derivedConfiguration = configuration.derive(new ExecuteContextConnectionProvider());
this.data = new DataMap();
this.query = query;
this.routine = routine;
this.converterScope = new DefaultConverterScope(derivedConfiguration, data);
if (routine != null) {
this.batch = false;
this.batchQueries = null;
this.batchRows = null;
this.batchSQL = null;
}
else if (batchQueries != null) {
this.batch = true;
this.batchQueries = batchQueries;
this.batchRows = new int[batchQueries.length];
this.batchSQL = new String[batchQueries.length];
Arrays.fill(this.batchRows, -1);
}
else if (query == null) {
this.batch = false;
this.batchQueries = null;
this.batchRows = null;
this.batchSQL = null;
}
else {
this.batch = false;
this.batchQueries = null;
this.batchRows = null;
this.batchSQL = null;
}
clean();
}
@Override
public final ConverterScope converterScope() {
return converterScope;
}
@Override
public final Instant creationTime() {
return creationTime;
}
@Override
public final Map<Object, Object> data() {
return data;
}
@Override
public final Object data(Object key) {
return data.get(key);
}
@Override
public final Object data(Object key, Object value) {
return data.put(key, value);
}
@Override
public final ExecuteType type() {
// This can only be a routine
if (routine != null) {
return ExecuteType.ROUTINE;
}
// This can only be a BatchSingle or BatchMultiple execution
else if (batch) {
return ExecuteType.BATCH;
}
// Any other type of query
else if (query != null) {
if (query instanceof ResultQuery) {
return ExecuteType.READ;
}
else if (query instanceof Insert
|| query instanceof Update
|| query instanceof Delete
|| query instanceof Merge) {
return ExecuteType.WRITE;
}
else if (query instanceof DDLQuery) {
return ExecuteType.DDL;
}
// Analyse SQL in plain SQL queries:
else {
String s = query.getSQL().toLowerCase(renderLocale(configuration().settings()));
// TODO: Use a simple lexer to parse SQL here. Potentially, the
// SQL Console's SQL formatter could be used...?
if (s.matches("^(with\\b.*?\\bselect|select|explain)\\b.*?"))
return ExecuteType.READ;
// These are sample DML statements. There may be many more
else if (s.matches("^(insert|update|delete|merge|replace|upsert|lock)\\b.*?"))
return ExecuteType.WRITE;
// These are only sample DDL statements. There may be many more
else if (s.matches("^(create|alter|drop|truncate|grant|revoke|analyze|comment|flashback|enable|disable)\\b.*?"))
return ExecuteType.DDL;
// JDBC escape syntax for routines
else if (s.matches("^\\s*\\{\\s*(\\?\\s*=\\s*)call.*?"))
return ExecuteType.ROUTINE;
// Vendor-specific calling of routines / procedural blocks
else if (s.matches("^(call|begin|declare)\\b.*?"))
return ExecuteType.ROUTINE;
}
}
// Fetching JDBC result sets, e.g. with SQL.fetch(ResultSet)
else if (resultSet != null) {
return ExecuteType.READ;
}
// No query available
return ExecuteType.OTHER;
}
@Override
public final Query query() {
return query;
}
@Override
public final Query[] batchQueries() {
return batch ? batchQueries
: query != null ? new Query[] { query }
: EMPTY_QUERY;
}
@Override
public final Routine<?> routine() {
return routine;
}
@Override
public final void sql(String s) {
this.sql = s;
// If this isn't a BatchMultiple query
if (batchSQL != null && batchSQL.length == 1)
batchSQL[0] = s;
}
@Override
public final String sql() {
return sql;
}
@Override
public final String[] batchSQL() {
return batch ? batchSQL
: routine != null || query != null ? new String[] { sql }
: EMPTY_STRING;
}
@Override
public final void statement(PreparedStatement s) {
this.statement = s;
}
@Override
public final PreparedStatement statement() {
return statement;
}
@Override
public final int statementExecutionCount() {
return statementExecutionCount;
}
@Override
public final void resultSet(ResultSet rs) {
this.resultSet = rs;
}
@Override
public final ResultSet resultSet() {
return resultSet;
}
@Override
public final Configuration configuration() {
return derivedConfiguration;
}
// [#4277] [#7569] The original configuration that was used to create the
// derived configuration in this ExecuteContext
final Configuration originalConfiguration() {
return originalConfiguration;
}
@Override
public final DSLContext dsl() {
return configuration().dsl();
}
@Override
public final Settings settings() {
return Tools.settings(configuration());
}
@Override
public final SQLDialect dialect() {
return Tools.configuration(configuration()).dialect();
}
@Override
public final SQLDialect family() {
return dialect().family();
}
@Override
public final void connectionProvider(ConnectionProvider provider) {
this.connectionProvider = provider;
}
@Override
public final Connection connection() {
// All jOOQ internals are expected to get a connection through this
// single method. It can thus be guaranteed, that every connection is
// wrapped by a ConnectionProxy, transparently, in order to implement
// Settings.getStatementType() correctly.
if (wrappedConnection == null && connectionProvider != null)
connection(connectionProvider, connectionProvider.acquire());
return wrappedConnection;
}
/**
* Initialise this {@link DefaultExecuteContext} with a pre-existing
* {@link Connection}.
* <p>
* [#3191] This is needed, e.g. when using
* {@link Query#keepStatement(boolean)}.
*/
final void connection(ConnectionProvider provider, Connection c) {
if (c != null) {
LOCAL_CONNECTION.set(c);
connection = c;
wrappedConnection = wrapConnection(provider, c);
}
}
private final SettingsEnabledConnection wrapConnection(ConnectionProvider provider, Connection c) {
return new SettingsEnabledConnection(new ProviderEnabledConnection(provider, c), derivedConfiguration.settings(), this);
}
final void incrementStatementExecutionCount() {
statementExecutionCount++;
}
final DefaultExecuteContext withStatementExecutionCount(int count) {
statementExecutionCount = count;
return this;
}
@Override
public final void record(Record r) {
this.record = r;
}
@Override
public final Record record() {
return record;
}
@Override
public final int recordLevel() {
return recordLevel;
}
@Override
public final int rows() {
return rows;
}
@Override
public final void rows(int r) {
this.rows = r;
// If this isn't a BatchMultiple query
if (batchRows != null && batchRows.length == 1)
batchRows[0] = r;
}
@Override
public final int[] batchRows() {
return batch ? batchRows
: routine != null || query != null ? new int[] { rows }
: EMPTY_INT;
}
@Override
public final void result(Result<?> r) {
this.result = r;
}
@Override
public final Result<?> result() {
return result;
}
@Override
public final int resultLevel() {
return resultLevel;
}
@Override
public final RuntimeException exception() {
return exception;
}
@Override
public final void exception(RuntimeException e) {
this.exception = Tools.translate(sql(), e);
if (Boolean.TRUE.equals(settings().isDebugInfoOnStackTrace())) {
// [#5570] Add jOOQ version and SQL Dialect info on the stack trace
// to help users write better bug reports.
// See http://stackoverflow.com/q/39712695/521799
StackTraceElement[] oldStack = exception.getStackTrace();
if (oldStack != null) {
StackTraceElement[] newStack = new StackTraceElement[oldStack.length + 1];
System.arraycopy(oldStack, 0, newStack, 1, oldStack.length);
newStack[0] = new StackTraceElement(
"org.jooq_" + Constants.VERSION + "." + dialect(),
"debug", null, -1);
exception.setStackTrace(newStack);
}
}
}
@Override
public final SQLException sqlException() {
return sqlException;
}
@Override
public final void sqlException(SQLException e) {
this.sqlException = e;
exception(Tools.translate(sql(), e));
}
@Override
public final SQLWarning sqlWarning() {
return sqlWarning;
}
@Override
public final void sqlWarning(SQLWarning e) {
this.sqlWarning = e;
}
@Override
public final String[] serverOutput() {
return serverOutput == null ? EMPTY_STRING : serverOutput;
}
@Override
public final void serverOutput(String[] output) {
this.serverOutput = output;
}
final class ExecuteContextConnectionProvider implements ConnectionProvider {
@NotNull
@Override
public final Connection acquire() {
// [#4277] Connections are acquired lazily in parent ExecuteContext. A child ExecuteContext
// may well need a Connection earlier than the parent, in case of which acquisition is
// forced in the parent as well.
if (connection == null)
DefaultExecuteContext.this.connection();
return wrapConnection(this, connection);
}
@Override
public final void release(Connection c) {}
}
}