blob: 1816465377083e61efdc209c8731b80126cb1df0 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.databaseaccess;
import java.io.StringWriter;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.persistence.descriptors.DescriptorQueryManager;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.queries.ModifyQuery;
import org.eclipse.persistence.sessions.SessionProfiler;
/**
* INTERNAL:
* <p>ParameterizedSQLBatchWritingMechanism is a private class, used by the DatabaseAccessor. it provides the required
* behavior for batching statements, for write, with parameter binding turned on.</p>
*
* @since OracleAS TopLink 10<i>g</i> (9.0.4)
*/
public class ParameterizedSQLBatchWritingMechanism extends BatchWritingMechanism {
/**
* This member variable is used to keep track of the last SQL string that was executed
* by this mechanism. If the current string and previous string match then simply
* bind in the arguments, otherwise end previous batch and start a new batch
*/
protected DatabaseCall previousCall;
/**
* This variable contains a list of the parameters list passed into the query
*/
protected List<List> parameters;
protected DatabaseCall lastCallAppended;
public ParameterizedSQLBatchWritingMechanism() {
super();
}
public ParameterizedSQLBatchWritingMechanism(DatabaseAccessor databaseAccessor) {
this.databaseAccessor = databaseAccessor;
this.parameters = new ArrayList<>();
this.maxBatchSize = this.databaseAccessor.getLogin().getPlatform().getMaxBatchWritingSize();
if (this.maxBatchSize == 0) {
// the max size was not set on the platform - use default
this.maxBatchSize = DatabasePlatform.DEFAULT_PARAMETERIZED_MAX_BATCH_WRITING_SIZE;
}
}
/**
* INTERNAL:
* This method is called by the DatabaseAccessor to add this statement to the list of statements
* being batched. This call may result in the Mechanism executing the batched statements and
* possibly, switching out the mechanisms
*/
@Override
public void appendCall(AbstractSession session, DatabaseCall dbCall) {
if (dbCall.hasParameters()) {
//make an equality check on the String, because if we are caching statements then
//we will not have to perform the string comparison multiple times.
if (this.previousCall == null) {
this.previousCall = dbCall;
this.parameters.add(dbCall.getParameters());
} else {
if (this.previousCall.getSQLString().equals(dbCall.getSQLString()) && (this.parameters.size() < this.maxBatchSize)) {
this.parameters.add(dbCall.getParameters());
} else {
executeBatchedStatements(session);
this.previousCall = dbCall;
this.parameters.add(dbCall.getParameters());
}
}
// Store the largest queryTimeout on a single call for later use by the single statement in prepareBatchStatements
if (dbCall != null) {
cacheQueryTimeout(session, dbCall);
}
this.lastCallAppended = dbCall;
// feature for bug 4104613, allows users to force statements to flush on execution
if (((ModifyQuery) dbCall.getQuery()).forceBatchStatementExecution())
{
executeBatchedStatements(session);
}
} else {
executeBatchedStatements(session);
switchMechanisms(session, dbCall);
}
}
/**
* INTERNAL:
* This method is used to clear the batched statements without the need to execute the statements first
* This is used in the case of rollback.
*/
@Override
public void clear() {
this.previousCall = null;
//Bug#419326 : A clone may be holding a reference to this.parameters.
//So, instead of clearing the parameters, just initialize with a new reference.
this.parameters = new ArrayList<>();
this.statementCount = 0;
this.executionCount = 0;
this.queryTimeoutCache = DescriptorQueryManager.NoTimeout;
// bug 229831 : BATCH WRITING CAUSES MEMORY LEAKS WITH UOW
this.lastCallAppended = null;
}
/**
* INTERNAL:
* This method is used by the DatabaseAccessor to clear the batched statements in the
* case that a non batchable statement is being executed
*/
@Override
public void executeBatchedStatements(AbstractSession session) {
if (this.parameters.isEmpty()) {
return;
}
//Bug#419326 : Added below clone, clear and clone.executeBatch(session)
//Cloning the mechanism and clearing the current mechanism ensures that the current batch
//is not visible to recursive calls to executeBatchedStatements(session).
ParameterizedSQLBatchWritingMechanism currentBatch = (ParameterizedSQLBatchWritingMechanism) this.clone();
this.clear();
currentBatch.executeBatch(session);
}
/**
* INTERNAL:
* This method is added to execute and clear the batched statements on the cloned batch mechanism which
* is created in executeBatchedStatements(session).
*
* Introduced in fix for bug#419326.
*/
private void executeBatch(AbstractSession session) {
if (this.parameters.size() == 1) {
// If only one call, just execute normally.
try {
int rowCount = (Integer)this.databaseAccessor.basicExecuteCall(this.previousCall, null, session, false);
if (this.previousCall.hasOptimisticLock()) {
if (rowCount != 1) {
throw OptimisticLockException.batchStatementExecutionFailure();
}
}
} finally {
clear();
}
return;
}
try {
this.databaseAccessor.incrementCallCount(session);// Decrement occurs in close.
if (session.shouldLog(SessionLog.FINE, SessionLog.SQL)) {
session.log(SessionLog.FINER, SessionLog.SQL, "begin_batch_statements", null, this.databaseAccessor);
session.log(SessionLog.FINE, SessionLog.SQL, this.previousCall.getSQLString(), null, this.databaseAccessor, false);
// took this logging part from SQLCall
for (List callParameters : this.parameters) {
StringWriter writer = new StringWriter();
DatabaseCall.appendLogParameters(callParameters, this.databaseAccessor, writer, session);
session.log(SessionLog.FINE, SessionLog.SQL, writer.toString(), null, this.databaseAccessor, false);
}
session.log(SessionLog.FINER, SessionLog.SQL, "end_batch_statements", null, this.databaseAccessor);
}
//bug 4241441: need to keep track of rows modified and throw opti lock exception if needed
PreparedStatement statement = prepareBatchStatements(session);
// += is used as native batch writing can return a row count before execution.
this.executionCount += this.databaseAccessor.executeJDK12BatchStatement(statement, this.lastCallAppended, session, true);
this.databaseAccessor.writeStatementsCount++;
if (this.previousCall.hasOptimisticLock() && (this.executionCount != this.statementCount)) {
throw OptimisticLockException.batchStatementExecutionFailure();
}
} finally {
// Reset the batched sql string
//we MUST clear the mechanism here in order to append the new statement.
this.clear();
}
}
/**
* INTERNAL:
* Swaps out the Mechanism for the other Mechanism
*/
protected void switchMechanisms(AbstractSession session, DatabaseCall dbCall) {
this.databaseAccessor.setActiveBatchWritingMechanismToDynamicSQL();
this.databaseAccessor.getActiveBatchWritingMechanism(session).appendCall(session, dbCall);
}
/**
* INTERNAL:
* This method is used to build the parameterized batch statement for the JDBC2.0 specification
*/
protected PreparedStatement prepareBatchStatements(AbstractSession session) throws DatabaseException {
PreparedStatement statement = null;
try {
session.startOperationProfile(SessionProfiler.SqlPrepare, null, SessionProfiler.ALL);
try {
DatabasePlatform platform = session.getPlatform();
boolean shouldUnwrapConnection = platform.usesNativeBatchWriting();
statement = (PreparedStatement)this.databaseAccessor.prepareStatement(this.previousCall, session, shouldUnwrapConnection);
// Perform platform specific preparations
platform.prepareBatchStatement(statement, this.maxBatchSize);
if (this.queryTimeoutCache > DescriptorQueryManager.NoTimeout) {
// Set the query timeout that was cached during the multiple calls to appendCall
statement.setQueryTimeout(this.queryTimeoutCache);
}
// Iterate over the parameter lists that were batched.
int statementSize = this.parameters.size();
for (int statementIndex = 0; statementIndex < statementSize; ++statementIndex) {
List parameterList = this.parameters.get(statementIndex);
int size = parameterList.size();
for (int index = 0; index < size; index++) {
platform.setParameterValueInDatabaseCall(parameterList.get(index), statement, index+1, session);
}
// Batch the parameters to the statement.
this.statementCount++;
this.executionCount += platform.addBatch(statement);
}
} finally {
session.endOperationProfile(SessionProfiler.SqlPrepare, null, SessionProfiler.ALL);
}
} catch (SQLException exception) {
// If this is a connection from an external pool then closeStatement will close the connection.
// we must test the connection before that happens.
RuntimeException exceptionToThrow = this.databaseAccessor.processExceptionForCommError(session, exception, lastCallAppended);
try {
// Ensure that the statement is closed, but still ensure that the real exception is thrown.
this.databaseAccessor.closeStatement(statement, session, null);
} catch (SQLException closeException) {
}
if (exceptionToThrow == null){
throw DatabaseException.sqlException(exception, this.databaseAccessor, session, false);
}
throw exceptionToThrow;
} catch (RuntimeException exception) {
try {
// Ensure that the statement is closed, but still ensure that the real exception is thrown.
this.databaseAccessor.closeStatement(statement, session, null);
} catch (SQLException closeException) {
}
throw exception;
}
return statement;
}
public DatabaseCall getPreviousCall() {
return previousCall;
}
public void setPreviousCall(DatabaseCall previousCall) {
this.previousCall = previousCall;
}
public List<List> getParameters() {
return parameters;
}
public void setParameters(List<List> parameters) {
this.parameters = parameters;
}
public DatabaseCall getLastCallAppended() {
return lastCallAppended;
}
public void setLastCallAppended(DatabaseCall lastCallAppended) {
this.lastCallAppended = lastCallAppended;
}
}