blob: e670e6c2e463aa4c4afefa8a05f8d04c63199788 [file] [log] [blame]
/***************************************************************************
* Copyright (c) 2004 - 2008 Eike Stepper, Germany.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eike Stepper - initial API and implementation
**************************************************************************/
package org.eclipse.emf.cdo.server.internal.db.jdbc;
import org.eclipse.emf.cdo.common.id.CDOID;
import org.eclipse.emf.cdo.common.id.CDOIDUtil;
import org.eclipse.emf.cdo.common.revision.CDORevision;
import org.eclipse.emf.cdo.server.db.IAttributeMapping;
import org.eclipse.emf.cdo.server.internal.db.CDODBSchema;
import org.eclipse.emf.cdo.server.internal.db.ServerInfo;
import org.eclipse.emf.cdo.server.internal.db.bundle.OM;
import org.eclipse.emf.cdo.spi.common.revision.InternalCDORevision;
import org.eclipse.net4j.db.DBException;
import org.eclipse.net4j.db.DBUtil;
import org.eclipse.net4j.util.collection.Pair;
import org.eclipse.net4j.util.om.monitor.OMMonitor;
import org.eclipse.net4j.util.om.monitor.OMMonitor.Async;
import org.eclipse.net4j.util.om.trace.ContextTracer;
import org.eclipse.net4j.util.ref.ReferenceValueMap;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* @author Stefan Winkler
* @since 2.0
*/
public class PreparedStatementJDBCDelegate extends AbstractJDBCDelegate
{
/**
* Value for {@link #cachingFlag}: Guess if caching is needed
*/
public static final String CACHE_STMTS_GUESS = "guess";
/**
* Value for {@link #cachingFlag}: Turn caching on
*/
public static final String CACHE_STMTS_TRUE = "true";
/**
* Value for {@link #cachingFlag}: Turn caching off
*/
public static final String CACHE_STMTS_FALSE = "false";
private static final ContextTracer TRACER = new ContextTracer(OM.DEBUG, PreparedStatementJDBCDelegate.class);
private static final String SQL_UPDATE_REVISE_VERSION = " SET " + CDODBSchema.ATTRIBUTES_REVISED + " = ? WHERE "
+ CDODBSchema.ATTRIBUTES_ID + " = ? AND " + CDODBSchema.ATTRIBUTES_VERSION + " = ?";
private static final String SQL_UPDATE_REVISE_UNREVISED = " SET " + CDODBSchema.ATTRIBUTES_REVISED + " = ? WHERE "
+ CDODBSchema.ATTRIBUTES_ID + " = ? AND " + CDODBSchema.ATTRIBUTES_REVISED + " = 0";
private static final String SQL_INSERT_REFERENCE_WITH_DBID = " VALUES (?, ?, ?, ?, ?)";
private static final String SQL_INSERT_REFERENCE = " VALUES (?, ?, ?, ?)";
/**
* Cache for preparedStatements used in diverse methods
*/
private Map<CacheKey, WrappedPreparedStatement> statementCache = null;
/**
* Container for dirty statements. A statement is considered 'dirty', if addBatch was called, but not executeBatch.
*/
private Map<CacheKey, PreparedStatement> dirtyStatements = null;
/**
* This flag determines, if prepared statements should be cached within this delegate. Its value is guessed in
* {@link #postInitConnection()} based on the fact if the JDBC driver supports pooled statements. If it does, caching
* in the delegate is unnecessary.
*/
private boolean cacheStatements;
private CachingEnablement cachingEnablement;
public PreparedStatementJDBCDelegate()
{
}
public CachingEnablement getCachingEnablement()
{
return cachingEnablement;
}
public void setCachingEnablement(CachingEnablement cachingEnablement)
{
checkInactive();
this.cachingEnablement = cachingEnablement;
}
public void flush(OMMonitor monitor)
{
try
{
monitor.begin(dirtyStatements.size());
for (Entry<CacheKey, PreparedStatement> entry : dirtyStatements.entrySet())
{
try
{
int[] results;
Async async = monitor.forkAsync();
try
{
results = entry.getValue().executeBatch();
}
finally
{
async.stop();
}
if (TRACER.isEnabled())
{
TRACER.format("Executing batch for {0} [{1}]", entry.getKey().toString(), entry.getValue());
}
for (int result : results)
{
checkState(result == 1, "Batch execution did not return '1' for " + entry.getKey().toString());
}
}
catch (SQLException ex)
{
throw new DBException("Batch execution failed for " + entry.getKey().toString() + " [" + entry.getValue()
+ "]", ex);
}
}
}
finally
{
if (!cacheStatements)
{
if (TRACER.isEnabled())
{
TRACER.format("Closing prepared statements.");
}
for (PreparedStatement ps : dirtyStatements.values())
{
DBUtil.close(ps);
}
}
else
{
if (TRACER.isEnabled())
{
TRACER.format("Re-caching prepared statements.");
}
for (Entry<CacheKey, PreparedStatement> entry : dirtyStatements.entrySet())
{
cacheStatement(entry.getKey(), entry.getValue());
}
}
dirtyStatements.clear();
monitor.done();
}
}
@Override
protected void doActivate() throws Exception
{
super.doActivate();
dirtyStatements = new ReferenceValueMap.Strong<CacheKey, PreparedStatement>();
switch (cachingEnablement)
{
case ENABLED:
cacheStatements = true;
break;
case DISABLED:
cacheStatements = false;
break;
case GUESS:
try
{
cacheStatements = !getConnection().getMetaData().supportsStatementPooling();
}
catch (SQLException ex)
{
OM.LOG.warn("Failed to guess JDBC statement pooling. Activating cache, just to be sure ...", ex);
cacheStatements = true;
}
}
if (TRACER.isEnabled())
{
TRACER.trace("JDBC PreparedStatement caching is " + (cacheStatements ? "enabled." : "NOT enabled."));
}
if (cacheStatements)
{
// initialize cache ...
statementCache = new ReferenceValueMap.Soft<CacheKey, WrappedPreparedStatement>();
}
}
@Override
protected void doBeforeDeactivate() throws Exception
{
for (PreparedStatement ps : dirtyStatements.values())
{
DBUtil.close(ps);
}
dirtyStatements.clear();
if (cacheStatements)
{
for (WrappedPreparedStatement ps : statementCache.values())
{
DBUtil.close(ps.getWrappedStatement());
}
statementCache.clear();
}
super.doBeforeDeactivate();
}
/**
* Implementation of the hook which is called after selects.
*/
@Override
protected void releaseStatement(Statement stmt)
{
// leave open cached statements
if (!cacheStatements || !(stmt instanceof PreparedStatement))
{
super.releaseStatement(stmt);
}
// /* This code would guarantee that releaseStatement is only called
// for cached statements. However this looks through the whole hashmap
// and is thus too expensive to do in non-debugging mode. */
//
// else {
// if(!selectStatementsCache.containsValue(stmt)) {
// super.releaseStatement(stmt);
// }
// }
}
@Override
protected void doInsertAttributes(String tableName, CDORevision rev, List<IAttributeMapping> attributeMappings,
boolean withFullRevisionInfo)
{
boolean firstBatch = false;
InternalCDORevision revision = (InternalCDORevision)rev;
if (attributeMappings == null)
{
attributeMappings = Collections.emptyList();
}
PreparedStatement stmt = getDirtyStatement(StmtType.INSERT_ATTRIBUTES, tableName);
if (stmt == null && cacheStatements)
{
firstBatch = true;
stmt = getAndRemoveCachedStatement(StmtType.INSERT_ATTRIBUTES, tableName);
}
try
{
firstBatch = true;
if (stmt == null)
{
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ");
sql.append(tableName);
sql.append(" VALUES (?, ?, ");
if (withFullRevisionInfo)
{
sql.append("?, ?, ?, ?, ?, ?");
}
for (int i = 0; i < attributeMappings.size(); i++)
{
sql.append(", ?");
}
sql.append(")");
stmt = getConnection().prepareStatement(sql.toString());
}
int col = 1;
if (TRACER.isEnabled())
{
TRACER.trace(stmt.toString());
}
stmt.setLong(col++, CDOIDUtil.getLong(revision.getID()));
stmt.setInt(col++, revision.getVersion());
if (withFullRevisionInfo)
{
stmt.setInt(col++, ServerInfo.getDBID(revision.getCDOClass()));
stmt.setLong(col++, revision.getCreated());
stmt.setLong(col++, revision.getRevised());
stmt.setLong(col++, CDOIDUtil.getLong(revision.getResourceID()));
stmt.setLong(col++, CDOIDUtil.getLong((CDOID)revision.getContainerID()));
stmt.setInt(col++, revision.getContainingFeatureID());
}
for (IAttributeMapping attributeMapping : attributeMappings)
{
Object value = attributeMapping.getRevisionValue(revision);
if (value == null)
{
stmt.setNull(col++, attributeMapping.getField().getType().getCode());
}
else
{
stmt.setObject(col++, value);
}
}
if (firstBatch)
{
addDirtyStatement(StmtType.INSERT_ATTRIBUTES, tableName, stmt);
}
stmt.addBatch();
}
catch (SQLException e)
{
throw new DBException(e);
}
}
@Override
protected void doInsertReference(String tableName, int dbID, long source, int version, int index, long target)
{
boolean firstBatch = false;
PreparedStatement stmt = getDirtyStatement(StmtType.INSERT_REFERENCES, tableName);
if (stmt == null && cacheStatements)
{
firstBatch = true;
stmt = getAndRemoveCachedStatement(StmtType.INSERT_REFERENCES, tableName);
}
try
{
if (stmt == null)
{
firstBatch = true;
StringBuilder sql = new StringBuilder("INSERT INTO ");
sql.append(tableName);
sql.append(dbID != 0 ? SQL_INSERT_REFERENCE_WITH_DBID : SQL_INSERT_REFERENCE);
stmt = getConnection().prepareStatement(sql.toString());
}
int idx = 1;
if (dbID != 0)
{
stmt.setInt(idx++, dbID);
}
stmt.setLong(idx++, source);
stmt.setInt(idx++, version);
stmt.setInt(idx++, index);
stmt.setLong(idx++, target);
if (TRACER.isEnabled())
{
TRACER.trace(stmt.toString());
}
if (firstBatch)
{
addDirtyStatement(StmtType.INSERT_REFERENCES, tableName, stmt);
}
stmt.addBatch();
}
catch (SQLException e)
{
throw new DBException(e);
}
}
@Override
protected void doUpdateRevised(String tableName, long revisedStamp, long cdoid, int version)
{
PreparedStatement stmt = null;
if (cacheStatements)
{
stmt = getCachedStatement(StmtType.REVISE_VERSION, tableName);
}
try
{
if (stmt == null)
{
StringBuilder sql = new StringBuilder("UPDATE ");
sql.append(tableName);
sql.append(SQL_UPDATE_REVISE_VERSION);
stmt = getConnection().prepareStatement(sql.toString());
if (cacheStatements)
{
cacheStatement(StmtType.REVISE_VERSION, tableName, stmt);
}
}
stmt.setLong(1, revisedStamp);
stmt.setLong(2, cdoid);
stmt.setInt(3, version);
if (TRACER.isEnabled())
{
TRACER.trace(stmt.toString());
}
stmt.execute();
}
catch (SQLException e)
{
throw new DBException(e);
}
finally
{
if (!cacheStatements)
{
DBUtil.close(stmt);
}
}
}
@Override
protected void doUpdateRevised(String tableName, long revisedStamp, long cdoid)
{
PreparedStatement stmt = null;
if (cacheStatements)
{
stmt = getCachedStatement(StmtType.REVISE_UNREVISED, tableName);
}
try
{
if (stmt == null)
{
StringBuilder sql = new StringBuilder("UPDATE ");
sql.append(tableName);
sql.append(SQL_UPDATE_REVISE_UNREVISED);
stmt = getConnection().prepareStatement(sql.toString());
if (cacheStatements)
{
cacheStatement(StmtType.REVISE_UNREVISED, tableName, stmt);
}
}
stmt.setLong(1, revisedStamp);
stmt.setLong(2, cdoid);
if (TRACER.isEnabled())
{
TRACER.trace(stmt.toString());
}
stmt.execute();
}
catch (SQLException e)
{
throw new DBException(e);
}
finally
{
if (!cacheStatements)
{
DBUtil.close(stmt);
}
}
}
@Override
protected ResultSet doSelectRevisionAttributes(String tableName, long revisionId,
List<IAttributeMapping> attributeMappings, boolean hasFullRevisionInfo, String where) throws SQLException
{
// Because of the variable where clause, statement caching can not be
// based on table names. Instead, we build the sql in any case and
// use this as key (similar to what JDBC3 does ...).
StringBuilder builder = new StringBuilder();
builder.append("SELECT ");
builder.append(CDODBSchema.ATTRIBUTES_VERSION);
builder.append(", ");
builder.append(CDODBSchema.ATTRIBUTES_CREATED);
if (hasFullRevisionInfo)
{
builder.append(", ");
builder.append(CDODBSchema.ATTRIBUTES_REVISED);
builder.append(", ");
builder.append(CDODBSchema.ATTRIBUTES_RESOURCE);
builder.append(", ");
builder.append(CDODBSchema.ATTRIBUTES_CONTAINER);
builder.append(", ");
builder.append(CDODBSchema.ATTRIBUTES_FEATURE);
}
for (IAttributeMapping attributeMapping : attributeMappings)
{
builder.append(", ");
builder.append(attributeMapping.getField());
}
builder.append(" FROM ");
builder.append(tableName);
builder.append(" WHERE ");
builder.append(CDODBSchema.ATTRIBUTES_ID);
builder.append("= ? AND (");
builder.append(where);
builder.append(")");
String sql = builder.toString();
if (TRACER.isEnabled())
{
TRACER.format("{0} ({1})", sql, revisionId);
}
PreparedStatement pstmt = null;
if (cacheStatements)
{
pstmt = getCachedStatement(StmtType.GENERAL, sql);
if (pstmt == null)
{
pstmt = getConnection().prepareStatement(sql);
cacheStatement(StmtType.GENERAL, sql, pstmt);
}
}
else
{
/* no caching */
pstmt = getConnection().prepareStatement(sql);
}
pstmt.setLong(1, revisionId);
return pstmt.executeQuery();
}
@Override
protected ResultSet doSelectRevisionReferences(String tableName, long sourceId, int version, int dbFeatureID,
String where) throws SQLException
{
StringBuilder builder = new StringBuilder();
builder.append("SELECT ");
builder.append(CDODBSchema.REFERENCES_TARGET);
builder.append(" FROM ");
builder.append(tableName);
builder.append(" WHERE ");
if (dbFeatureID != 0)
{
builder.append(CDODBSchema.REFERENCES_FEATURE);
builder.append("= ? AND ");
}
builder.append(CDODBSchema.REFERENCES_SOURCE);
builder.append("= ? AND ");
builder.append(CDODBSchema.REFERENCES_VERSION);
builder.append("= ? ");
if (where != null)
{
builder.append(where);
}
builder.append(" ORDER BY ");
builder.append(CDODBSchema.REFERENCES_IDX);
String sql = builder.toString();
if (TRACER.isEnabled())
{
TRACER.trace(sql);
}
PreparedStatement pstmt = null;
if (cacheStatements)
{
pstmt = getCachedStatement(StmtType.GENERAL, sql);
if (pstmt == null)
{
pstmt = getConnection().prepareStatement(sql);
cacheStatement(StmtType.GENERAL, sql, pstmt);
}
}
else
{
/* no caching */
pstmt = getConnection().prepareStatement(sql);
}
int idx = 1;
if (dbFeatureID != 0)
{
pstmt.setInt(idx++, dbFeatureID);
}
pstmt.setLong(idx++, sourceId);
pstmt.setInt(idx++, version);
return pstmt.executeQuery();
}
/**
* Add a dirty statement to the dirty statements container.
*/
private void addDirtyStatement(StmtType type, String subKey, PreparedStatement stmt)
{
if (TRACER.isEnabled())
{
TRACER.format("Adding dirty statement: ({0},{1}) -> {2}", type, subKey, stmt);
}
dirtyStatements.put(new CacheKey(type, subKey), stmt);
}
/**
* Query the dirty statements container.
*
* @return
*/
private PreparedStatement getDirtyStatement(StmtType type, String subKey)
{
return dirtyStatements.get(CacheKey.useOnce(type, subKey));
}
/**
* Cache a prepared statement.
*/
private void cacheStatement(StmtType type, String subKey, PreparedStatement stmt)
{
cacheStatement(new CacheKey(type, subKey), stmt);
}
/**
* Cache a prepared statement with given key.
*/
private void cacheStatement(CacheKey key, PreparedStatement stmt)
{
if (TRACER.isEnabled())
{
TRACER.format("Adding cached statement: {0} -> {1}", key, stmt);
}
statementCache.put(key, new WrappedPreparedStatement(stmt));
}
/**
* Query the cache of prepared statements.
*/
private PreparedStatement getCachedStatement(StmtType type, String subKey)
{
WrappedPreparedStatement wrapped = statementCache.get(CacheKey.useOnce(type, subKey));
if (wrapped == null)
{
return null;
}
PreparedStatement stmt = wrapped.getWrappedStatement();
if (TRACER.isEnabled())
{
TRACER.format("Using cached statement: ({0},{1}) -> {2}", type, subKey, stmt);
}
return stmt;
}
private PreparedStatement getAndRemoveCachedStatement(StmtType type, String subKey)
{
WrappedPreparedStatement wrapped = statementCache.remove(CacheKey.useOnce(type, subKey));
if (wrapped == null)
{
return null;
}
PreparedStatement stmt = wrapped.unwrapStatement();
if (TRACER.isEnabled())
{
TRACER.format("Removing cached statement: ({0},{1}) -> {2}", type, subKey, stmt);
}
return stmt;
}
/**
* @author Stefan Winkler
*/
public static enum CachingEnablement
{
ENABLED, DISABLED, GUESS
}
/**
* Statement type as first part of the statement cache key.
*
* @author Stefan Winkler
*/
private static enum StmtType
{
INSERT_ATTRIBUTES, INSERT_REFERENCES, REVISE_VERSION, REVISE_UNREVISED, GENERAL
}
/**
* Convenience definition for Pair<StmtType, String>
*
* @author Stefan Winkler
*/
private static class CacheKey extends Pair<StmtType, String>
{
public CacheKey(StmtType type, String subKey)
{
super(type, subKey);
}
private static CacheKey useOnceKey = new CacheKey(StmtType.GENERAL, "");
/**
* Memory-resource-friendly method which uses a single static field, modifies it and returns it to be used at once,
* e.g., to use it in a cache lookup. Do not store a reference to the result!!!
*/
public static CacheKey useOnce(StmtType type, String subKey)
{
useOnceKey.setElement1(type);
useOnceKey.setElement2(subKey);
return useOnceKey;
}
}
}