/*
* Copyright 2006-2012 The Scriptella Project Team.
*
* 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 scriptella.jdbc;
import scriptella.util.IOUtils;
import scriptella.util.LRUMap;
import java.io.Closeable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import static scriptella.util.CollectionUtils.isEmpty;
/**
* Statements cache for {@link JdbcConnection}.
* TODO Extract statement handling policy interface and provide 2 implementations for normal and batched mode. (+1 for testing)
* TODO Use wrapper class for Jdbc ConnectionParameters to store typesafe parameters and overridable factories
*
* @author Fyodor Kupolov
* @version 1.0
*/
class StatementCache implements Closeable {
private Map<String, StatementWrapper> map;
private final Connection connection;
private final JdbcTypesConverter converter;
private int batchSize;
private StatementWrapper.Batched sharedBatchedStatement; //see getter for description
private int fetchSize;
/**
* Creates a statement cache for specified connection.
*
* @param connection connection to create cache for.
* @param size cache size, 0 or negative means disable cache.
* @param batchSize size of prepared statements batch.
* @param fetchSize see {@link java.sql.Statement#setFetchSize(int)}
*/
public StatementCache(Connection connection, final int size, final int batchSize, final int fetchSize) {
this.connection = connection;
this.batchSize = batchSize;
this.converter = new JdbcTypesConverter();
this.fetchSize = fetchSize;
if (size > 0) { //if cache is enabled
map = new CacheMap(size);
}
}
/**
* Prepares a statement.
* <p>The sql is used as a key to lookup a {@link StatementWrapper},
* if cache miss the statement is created and put to cache.
*
* @param sql statement SQL.
* @param params parameters for SQL.
* @return a wrapper for specified SQL.
* @throws SQLException if DB reports an error
* @see StatementWrapper
*/
public StatementWrapper<?> prepare(final String sql, final List<Object> params) throws SQLException {
//In batch mode always use Batched statement for sql without parameters
if (isBatchMode() && isEmpty(params)) {
StatementWrapper.Batched batchedSt = getSharedBatchStatement();
batchedSt.setSql(sql);
return batchedSt;
}
StatementWrapper<?> sw = map == null ? null : map.get(sql);
if (sw == null) { //If not cached
if (isEmpty(params)) {
sw = create(sql);
} else {
sw = prepare(sql);
}
put(sql, sw);
} else if (sw instanceof StatementWrapper.Simple) {
//if simple statement is obtained second time - use prepared to improve performance
sw.close(); //closing unused statement
put(sql, sw = prepare(sql));
}
sw.setParameters(params);
return sw;
}
/**
* Testable template method to create simple statement
*/
protected StatementWrapper create(final String sql) throws SQLException {
Statement statement = connection.createStatement();
if (fetchSize != 0) {
statement.setFetchSize(fetchSize);
}
return new StatementWrapper.Simple(statement, sql, converter);
}
/**
* Testable template method to create prepared statement
*/
protected StatementWrapper.Prepared prepare(final String sql) throws SQLException {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
if (fetchSize != 0) {
preparedStatement.setFetchSize(fetchSize);
}
if (isBatchMode()) {
return new StatementWrapper.BatchedPrepared(preparedStatement, converter, batchSize);
} else {
return new StatementWrapper.Prepared(preparedStatement, converter);
}
}
private boolean isBatchMode() {
return batchSize > 0;
}
/**
* Returns an instance of StatementWrapper.Batched shared on the instance level.
* <p>Since each ETL element has its own cache, we are using a shared statement.
* This is critical in batch mode to allow grouping different statements in one batch.
*
* @return instance of shared statement.
* @throws SQLException if error occurs
*/
protected StatementWrapper.Batched getSharedBatchStatement() throws SQLException {
if (sharedBatchedStatement == null) {
sharedBatchedStatement = new StatementWrapper.Batched(connection.createStatement(), converter, batchSize);
}
return sharedBatchedStatement;
}
private void put(String key, StatementWrapper entry) {
if (map != null) {
map.put(key, entry);
}
}
/**
* Notifies cache that specified statement is no longer in use.
* Close method is invoked on statements pending release after removing from cache.
*
* @param sw released statement.
*/
public void releaseStatement(StatementWrapper sw) {
if (sw == null) {
throw new IllegalArgumentException("Released statement cannot be null");
}
//if caching disabled or simple statement - close it
if (map == null) {
sw.close();
} else {
sw.clear();
}
}
public void close() {
if (map != null) {
//closing statements
IOUtils.closeSilently(map.values());
map = null;
}
}
/**
* Flushes pending batches.
*
* @throws SQLException if DB error occurs.
*/
public void flush() throws SQLException {
if (isBatchMode()) {
if (sharedBatchedStatement != null) {
sharedBatchedStatement.flush();
}
if (map != null) {
for (StatementWrapper sw : map.values()) {
sw.flush();
}
}
}
}
/**
* LRU Map implementation for statement cache.
*/
private static class CacheMap extends LRUMap<String, StatementWrapper> {
private static final long serialVersionUID = 1;
public CacheMap(int size) {
super(size);
}
protected void onEldestEntryRemove(Map.Entry<String, StatementWrapper> eldest) {
eldest.getValue().close();
}
}
}