SQLExecutor.java
/*
** Module : SQLExecutor.java
** Abstract : Execute SQLs and potentially log them
**
** Copyright (c) 2023-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 RAA 20230607 First revision with basic usage.
** RAA 20230608 Integrated configuration for each database.
** RAA 20230609 Replaced Configuration with SQLStatementLoggerConfiguration.
** 002 RAA 20230615 Refactored so that all execute methods receive the SQL.
** RAA 20230724 Added some specific types of Statement to avoid the direct use of ConsumerWithException.
** Added LambaLoggerType.
** 003 RAA 20231011 Refactored cases that allow the use of sensitive data.
** 004 ICP 20241114 Added calls to update the query counter JMX.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist.orm;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.function.*;
import java.util.stream.Collectors;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.Database;
import com.goldencode.p2j.persist.DatabaseManager;
/**
* Class responsible with executing SQLs.
* If logging is intended, then the execution job will be passed to {@link SQLStatementLogger}.
*/
public class SQLExecutor
{
/** The static singleton instance, initially null for lazy initialization. */
private static SQLExecutor instance = null;
/** Singleton instance of {@link QueryCounterJMX} for managing query counting operations. */
private static final QueryCounterJMX SIMPLE_QUERY_COUNTER =
QueryCounterJMX.getInstance(FwdServerJMX.QueryCounterEnum.QueryCounter);
/** Usually, SQLs are executed one by one. There are methods that take care of
* SQLs executed in proper batches.
*/
private final int defaultBatchSize = 1;
/**
* The private default constructor.
*/
private SQLExecutor()
{
SIMPLE_QUERY_COUNTER.updateSupplier(SQLQueryCounterConfiguration::getConfiguration);
List<String> uniqueDatabases = DatabaseManager.getAllDatabases()
.stream()
.map(Database::getName)
.distinct()
.collect(Collectors.toList());
SIMPLE_QUERY_COUNTER.bootstrap(uniqueDatabases);
}
/**
* Static accessor to singleton with lazy initialization.
*
* @return The {@code SQLExecutor} singleton instance.
*/
public static synchronized SQLExecutor getInstance()
{
if (instance == null)
{
instance = new SQLExecutor();
}
return instance;
}
/**
* Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
* return without printing anything.
*
* @param <T>
* The statement type.
* @param database
* The reference {@code Database}.
* @param sql
* String representing the SQL used in the statement.
* @param r
* A {@link ConsumerWithException} that handles the execution of a statement.
* @param receiver
* Receiver denoting the statement that will be executed.
*/
public <T> void execute(Database database, String sql, ConsumerWithException<T> r, T receiver)
throws SQLException
{
execute(database, sql, r, receiver, null, defaultBatchSize, LambdaLoggerType.CONSUMER);
}
/**
* Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
* return without printing anything.
*
* @param <T>
* The statement type.
* @param database
* The reference {@code Database}.
* @param sql
* String representing the SQL used in the statement.
* @param r
* A {@link ConsumerWithException} that handles the execution of a statement.
* @param receiver
* Receiver denoting the statement that will be executed.
* @param batchSize
* How many SQLs are executed in this batch.
*/
public <T> void execute(Database database,
String sql,
ConsumerWithException<T> r,
T receiver,
int batchSize)
throws SQLException
{
execute(database, sql, r, receiver, null, batchSize, LambdaLoggerType.CONSUMER);
}
/**
* Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
* return without printing anything. It will use the {@code toString()} method of the {@code Statement}.
*
* @param <T>
* The statement type.
* @param <U>
* The argument type.
* @param database
* The reference {@code Database}.
* @param r
* A {@link BiConsumerWithException} that handles the execution of a statement with a parameter.
* @param receiver
* Receiver denoting the statement that will be executed.
* @param otherReceiver
* The parameter that will be given to the execution.
*/
public <T, U> void execute(Database database,
BiConsumerWithException<T, U> r,
T receiver,
U otherReceiver)
throws SQLException
{
execute(database, null, r, receiver, otherReceiver, defaultBatchSize, LambdaLoggerType.BICONSUMER);
}
/**
* Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
* return without printing anything. It will use the {@code toString()} method of the {@code Statement}.
*
* @param <T>
* The statement type.
* @param <U>
* The result type.
* @param database
* The reference {@code Database}.
* @param sql
* The SQL used in the statement.
* @param r
* {@link FunctionWithException} that handles the execution of a statement and returns a result.
* @param receiver
* Receiver denoting the statement that will be executed.
*
* @return The result generated by the execution of the statement.
*/
public <T, U> U execute(Database database, String sql, FunctionWithException<T, U> r, T receiver)
throws SQLException
{
return execute(database, sql, r, receiver, null, defaultBatchSize, LambdaLoggerType.FUNCTION);
}
/**
* Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
* return without printing anything.
*
* @param <T>
* The statement type.
* @param <U>
* The argument type or the result type, depending on {@code LambaLoggerType}.
* @param database
* The reference {@code Database}.
* @param sql
* The SQL used in the statement.
* @param r
* A {@link LambdaLoggerType} that handles the execution of a statement.
* @param receiver
* Receiver denoting the statement that will be executed.
* @param otherReceiver
* The parameter that will be given to the execution.
* @param batchSize
* How many SQLs are executed in this batch.
* @param type
* The type of the receiver.
*
* @return The result generated by the execution of the statement, if requested, or null otherwise.
*/
private <T, U> U execute(Database database,
String sql,
LambdaLogger r,
T receiver,
U otherReceiver,
int batchSize,
LambdaLoggerType type)
throws SQLException
{
String databaseName = database.getName();
SQLStatementLoggerConfiguration config = SQLStatementLoggerConfiguration.
getConfiguration(databaseName);
U result = null;
SIMPLE_QUERY_COUNTER.updateCount(databaseName, new QueryInfo(sql, receiver));
switch (config.getLoggingType())
{
case NONE:
return executeImpl(r, receiver, otherReceiver, type);
case LOGGING:
try
{
result = executeImpl(r, receiver, otherReceiver, type);
}
finally
{
boolean sendFullStatement = sql == null;
SQLStatementLogger.getLogger().log(databaseName,
sendFullStatement ? receiver.toString() : sql,
sendFullStatement,
config,
batchSize,
null);
}
break;
case PROFILING:
long start = System.nanoTime();
try
{
result = executeImpl(r, receiver, otherReceiver, type);
}
finally
{
long finish = System.nanoTime();
String statement;
boolean isFullStatement;
if (config.isAllowSensitive() || sql == null)
{
statement = receiver.toString();
isFullStatement = true;
}
else
{
statement = sql;
isFullStatement = false;
}
SQLStatementLogger.getLogger().log(databaseName,
statement,
isFullStatement,
config,
batchSize,
(finish - start) / 1000000.0);
}
break;
}
return result;
}
/**
*
* @param <T>
* The statement type.
* @param <U>
* The argument type or the result type, depending on {@code LambaLoggerType}.
* @param r
* A {@link LambdaLoggerType} that handles the execution of a statement.
* @param receiver
* Receiver denoting the statement that will be executed.
* @param otherReceiver
* The parameter that will be given to the execution.
* @param type
* The type of the receiver.
*
* @return U
* The result.
*/
private <T, U> U executeImpl(LambdaLogger r, T receiver, U otherReceiver, LambdaLoggerType type)
throws SQLException
{
switch (type)
{
case CONSUMER:
((ConsumerWithException<T>) r).accept(receiver);
break;
case BICONSUMER:
((BiConsumerWithException<T, U>) r).accept(receiver, otherReceiver);
break;
case FUNCTION:
return ((FunctionWithException<T, U>) r).apply(receiver);
default:
break;
}
return null;
}
/**
* Basic interface that groups the functional interfaces used for logging statements.
* For the moment, one may use only {@link ConsumerWithException}, {@link BiConsumerWithException} or
* {@link FunctionWithException}.
*/
public interface LambdaLogger
{
}
/**
* Enumeration used to distinguish between different {@link LambdaLogger}s. Depending on their type,
* the behavior varies (returning/not returning a result, using/not using a parameter, etc.).
*/
protected enum LambdaLoggerType
{
/** Type that matches the {@code Consumer} behavior: uses one thing and returns nothing. */
CONSUMER,
/** Type that matches the {@code BiConsumer} behavior: uses two things and returns nothing. */
BICONSUMER,
/** Type that matches the {@code Function} behavior: uses and returns one thing. */
FUNCTION;
}
/**
* Functional interface, similar to {@link Consumer}, but with the capability to throw an Exception.
*
* @param <T>
* The type of the argument.
*/
@FunctionalInterface
public interface ConsumerWithException<T>
extends LambdaLogger
{
/**
* Performs this operation on the given argument.
*
* @param t
* The input argument.
*/
void accept(T t)
throws SQLException;
}
/**
* A more specific {@code ConsumerWithException}, applied to {@code PreparedStatement} only.
*/
public interface DMLStatement
extends ConsumerWithException<PreparedStatement>
{
}
/**
* Functional interface, similar to {@link BiConsumer}, but with the capability to throw an Exception.
*
* @param <T>
* The type of the first argument.
* @param <U>
* The type of the second argument.
*/
@FunctionalInterface
public interface BiConsumerWithException<T, U>
extends LambdaLogger
{
/**
* Performs this operation on the given arguments.
*
* @param t
* The first input argument.
* @param u
* The second input argument.
*/
void accept(T t, U u)
throws SQLException;
}
/**
* Functional interface, similar to {@link Function}, but with the capability to throw an Exception.
*
* @param <T>
* The type of the first argument.
* @param <R>
* The type of the second argument.
*/
@FunctionalInterface
public interface FunctionWithException<T, R>
extends LambdaLogger
{
/**
* Applies this function to the given argument.
*
* @param t
* The function argument.
*
* @return The function result.
*/
R apply(T t)
throws SQLException;
}
/**
* A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and
* {@code ResultSet} only.
*/
public interface QueryStatement
extends FunctionWithException<PreparedStatement, ResultSet>
{
}
/**
* A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and
* {@code Integer} only.
*/
public interface UpdateStatement
extends FunctionWithException<PreparedStatement, Integer>
{
}
/**
* A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and
* {@code int[]} only.
*/
public interface UpdateBatchStatement
extends FunctionWithException<PreparedStatement, int[]>
{
}
}