序
本文主要研究一下mysql jdbc的prepareStatement
prepareStatement
java/sql/Connection.java
/**
* Creates a <code>PreparedStatement</code> object for sending
* parameterized SQL statements to the database.
* <P>
* A SQL statement with or without IN parameters can be
* pre-compiled and stored in a <code>PreparedStatement</code> object. This
* object can then be used to efficiently execute this statement
* multiple times.
*
* <P><B>Note:</B> This method is optimized for handling
* parametric SQL statements that benefit from precompilation. If
* the driver supports precompilation,
* the method <code>prepareStatement</code> will send
* the statement to the database for precompilation. Some drivers
* may not support precompilation. In this case, the statement may
* not be sent to the database until the <code>PreparedStatement</code>
* object is executed. This has no direct effect on users; however, it does
* affect which methods throw certain <code>SQLException</code> objects.
* <P>
* Result sets created using the returned <code>PreparedStatement</code>
* object will by default be type <code>TYPE_FORWARD_ONLY</code>
* and have a concurrency level of <code>CONCUR_READ_ONLY</code>.
* The holdability of the created result sets can be determined by
* calling {@link #getHoldability}.
*
* @param sql an SQL statement that may contain one or more '?' IN
* parameter placeholders
* @return a new default <code>PreparedStatement</code> object containing the
* pre-compiled SQL statement
* @exception SQLException if a database access error occurs
* or this method is called on a closed connection
*/
PreparedStatement prepareStatement(String sql)
throws SQLException;
java.sql.Connection定义了prepareStatement方法,根据sql创建PreparedStatement
ConnectionImpl
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/ConnectionImpl.java
/**
* A SQL statement with or without IN parameters can be pre-compiled and
* stored in a PreparedStatement object. This object can then be used to
* efficiently execute this statement multiple times.
* <p>
* <B>Note:</B> This method is optimized for handling parametric SQL
* statements that benefit from precompilation if the driver supports
* precompilation. In this case, the statement is not sent to the database
* until the PreparedStatement is executed. This has no direct effect on
* users; however it does affect which method throws certain
* java.sql.SQLExceptions
* </p>
* <p>
* MySQL does not support precompilation of statements, so they are handled
* by the driver.
* </p>
*
* @param sql
* a SQL statement that may contain one or more '?' IN parameter
* placeholders
* @return a new PreparedStatement object containing the pre-compiled
* statement.
* @exception SQLException
* if a database access error occurs.
*/
public java.sql.PreparedStatement prepareStatement(String sql)
throws SQLException {
return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE,
DEFAULT_RESULT_SET_CONCURRENCY);
}
mysql jdbc的ConnectionImpl实现了prepareStatement方法,根据注释,预编译主要是driver来处理
prepareStatement
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/ConnectionImpl.java
/**
* JDBC 2.0 Same as prepareStatement() above, but allows the default result
* set type and result set concurrency type to be overridden.
*
* @param sql
* the SQL query containing place holders
* @param resultSetType
* a result set type, see ResultSet.TYPE_XXX
* @param resultSetConcurrency
* a concurrency type, see ResultSet.CONCUR_XXX
* @return a new PreparedStatement object containing the pre-compiled SQL
* statement
* @exception SQLException
* if a database-access error occurs.
*/
public synchronized java.sql.PreparedStatement prepareStatement(String sql,
int resultSetType, int resultSetConcurrency) throws SQLException {
checkClosed();
//
// FIXME: Create warnings if can't create results of the given
// type or concurrency
//
PreparedStatement pStmt = null;
boolean canServerPrepare = true;
String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;
if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
}
if (this.useServerPreparedStmts && canServerPrepare) {
if (this.getCachePreparedStatements()) {
synchronized (this.serverSideStatementCache) {
pStmt = (com.mysql.jdbc.ServerPreparedStatement)this.serverSideStatementCache.remove(sql);
if (pStmt != null) {
((com.mysql.jdbc.ServerPreparedStatement)pStmt).setClosed(false);
pStmt.clearParameters();
}
if (pStmt == null) {
try {
pStmt = ServerPreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,
this.database, resultSetType, resultSetConcurrency);
if (sql.length() < getPreparedStatementCacheSqlLimit()) {
((com.mysql.jdbc.ServerPreparedStatement)pStmt).isCached = true;
}
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
// Punt, if necessary
if (getEmulateUnsupportedPstmts()) {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
if (sql.length() < getPreparedStatementCacheSqlLimit()) {
this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
}
} else {
throw sqlEx;
}
}
}
}
} else {
try {
pStmt = ServerPreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,
this.database, resultSetType, resultSetConcurrency);
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
// Punt, if necessary
if (getEmulateUnsupportedPstmts()) {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
} else {
throw sqlEx;
}
}
}
} else {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
return pStmt;
}
prepareStatement首先根据useServerPreparedStmts以及getEmulateUnsupportedPstmts来判断是否要通过canHandleAsServerPreparedStatement判断canServerPrepare;之后在useServerPreparedStmts及canServerPrepare为true时,根据cachePreparedStatements做ServerPreparedStatement的处理;如果不开启serverPrepare则执行clientPrepareStatement
canHandleAsServerPreparedStatement
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/ConnectionImpl.java
private boolean canHandleAsServerPreparedStatement(String sql)
throws SQLException {
if (sql == null || sql.length() == 0) {
return true;
}
if (!this.useServerPreparedStmts) {
return false;
}
if (getCachePreparedStatements()) {
synchronized (this.serverSideStatementCheckCache) {
Boolean flag = (Boolean)this.serverSideStatementCheckCache.get(sql);
if (flag != null) {
return flag.booleanValue();
}
boolean canHandle = canHandleAsServerPreparedStatementNoCache(sql);
if (sql.length() < getPreparedStatementCacheSqlLimit()) {
this.serverSideStatementCheckCache.put(sql,
canHandle ? Boolean.TRUE : Boolean.FALSE);
}
return canHandle;
}
}
return canHandleAsServerPreparedStatementNoCache(sql);
}
canHandleAsServerPreparedStatement首先判断useServerPreparedStmts,之后若cachePreparedStatements为true则做serverSideStatementCheckCache判断,最后都会通过canHandleAsServerPreparedStatementNoCache进行判断
canHandleAsServerPreparedStatementNoCache
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/ConnectionImpl.java
private boolean canHandleAsServerPreparedStatementNoCache(String sql)
throws SQLException {
// Can't use server-side prepare for CALL
if (StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql, "CALL")) {
return false;
}
boolean canHandleAsStatement = true;
if (!versionMeetsMinimum(5, 0, 7) &&
(StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql, "SELECT")
|| StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql,
"DELETE")
|| StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql,
"INSERT")
|| StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql,
"UPDATE")
|| StringUtils.startsWithIgnoreCaseAndNonAlphaNumeric(sql,
"REPLACE"))) {
// check for limit ?[,?]
/*
* The grammar for this (from the server) is: ULONG_NUM | ULONG_NUM
* ',' ULONG_NUM | ULONG_NUM OFFSET_SYM ULONG_NUM
*/
int currentPos = 0;
int statementLength = sql.length();
int lastPosToLook = statementLength - 7; // "LIMIT ".length()
boolean allowBackslashEscapes = !this.noBackslashEscapes;
char quoteChar = this.useAnsiQuotes ? '"' : '\'';
boolean foundLimitWithPlaceholder = false;
while (currentPos < lastPosToLook) {
int limitStart = StringUtils.indexOfIgnoreCaseRespectQuotes(
currentPos, sql, "LIMIT ", quoteChar,
allowBackslashEscapes);
if (limitStart == -1) {
break;
}
currentPos = limitStart + 7;
while (currentPos < statementLength) {
char c = sql.charAt(currentPos);
//
// Have we reached the end
// of what can be in a LIMIT clause?
//
if (!Character.isDigit(c) && !Character.isWhitespace(c)
&& c != ',' && c != '?') {
break;
}
if (c == '?') {
foundLimitWithPlaceholder = true;
break;
}
currentPos++;
}
}
canHandleAsStatement = !foundLimitWithPlaceholder;
} else if (StringUtils.startsWithIgnoreCaseAndWs(sql, "CREATE TABLE")) {
canHandleAsStatement = false;
} else if (StringUtils.startsWithIgnoreCaseAndWs(sql, "DO")) {
canHandleAsStatement = false;
} else if (StringUtils.startsWithIgnoreCaseAndWs(sql, "SET")) {
canHandleAsStatement = false;
}
return canHandleAsStatement;
}
canHandleAsServerPreparedStatementNoCache方法针对call、create table、do、set返回false,其他的针对小于5.0.7版本的做特殊判断,其余的默认返回true
clientPrepareStatement
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/ConnectionImpl.java
/** A cache of SQL to parsed prepared statement parameters. */
private CacheAdapter<String, ParseInfo> cachedPreparedStatementParams;
public java.sql.PreparedStatement clientPrepareStatement(String sql,
int resultSetType, int resultSetConcurrency,
boolean processEscapeCodesIfNeeded) throws SQLException {
checkClosed();
String nativeSql = processEscapeCodesIfNeeded && getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;
PreparedStatement pStmt = null;
if (getCachePreparedStatements()) {
PreparedStatement.ParseInfo pStmtInfo = this.cachedPreparedStatementParams.get(nativeSql);
if (pStmtInfo == null) {
pStmt = com.mysql.jdbc.PreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,
this.database);
this.cachedPreparedStatementParams.put(nativeSql, pStmt
.getParseInfo());
} else {
pStmt = new com.mysql.jdbc.PreparedStatement(getLoadBalanceSafeProxy(), nativeSql,
this.database, pStmtInfo);
}
} else {
pStmt = com.mysql.jdbc.PreparedStatement.getInstance(getLoadBalanceSafeProxy(), nativeSql,
this.database);
}
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
return pStmt;
}
clientPrepareStatement在cachePreparedStatements为true时会从cachedPreparedStatementParams(缓存的key为nativeSql,value为ParseInfo
)去获取ParseInfo,获取不到则执行com.mysql.jdbc.PreparedStatement.getInstance再放入缓存,获取到ParseInfo则通过com.mysql.jdbc.PreparedStatement(getLoadBalanceSafeProxy(), nativeSql,this.database, pStmtInfo)创建PreparedStatement;如果为false则直接通过com.mysql.jdbc.PreparedStatement.getInstance来创建
PreparedStatement.getInstance
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/PreparedStatement.java
/**
* Creates a prepared statement instance -- We need to provide factory-style
* methods so we can support both JDBC3 (and older) and JDBC4 runtimes,
* otherwise the class verifier complains when it tries to load JDBC4-only
* interface classes that are present in JDBC4 method signatures.
*/
protected static PreparedStatement getInstance(MySQLConnection conn, String sql,
String catalog) throws SQLException {
if (!Util.isJdbc4()) {
return new PreparedStatement(conn, sql, catalog);
}
return (PreparedStatement) Util.handleNewInstance(
JDBC_4_PSTMT_3_ARG_CTOR, new Object[] { conn, sql, catalog }, conn.getExceptionInterceptor());
}
getInstance方法对于非jdbc4的直接new一个PreparedStatement,若使用了jdbc4则通过Util.handleNewInstance使用JDBC_4_PSTMT_3_ARG_CTOR的构造器反射创建
JDBC_4_PSTMT_3_ARG_CTOR
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/PreparedStatement.java
public class PreparedStatement extends com.mysql.jdbc.StatementImpl implements
java.sql.PreparedStatement {
private static final Constructor<?> JDBC_4_PSTMT_2_ARG_CTOR;
private static final Constructor<?> JDBC_4_PSTMT_3_ARG_CTOR;
private static final Constructor<?> JDBC_4_PSTMT_4_ARG_CTOR;
static {
if (Util.isJdbc4()) {
try {
JDBC_4_PSTMT_2_ARG_CTOR = Class.forName(
"com.mysql.jdbc.JDBC4PreparedStatement")
.getConstructor(
new Class[] { MySQLConnection.class, String.class });
JDBC_4_PSTMT_3_ARG_CTOR = Class.forName(
"com.mysql.jdbc.JDBC4PreparedStatement")
.getConstructor(
new Class[] { MySQLConnection.class, String.class,
String.class });
JDBC_4_PSTMT_4_ARG_CTOR = Class.forName(
"com.mysql.jdbc.JDBC4PreparedStatement")
.getConstructor(
new Class[] { MySQLConnection.class, String.class,
String.class, ParseInfo.class });
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
} else {
JDBC_4_PSTMT_2_ARG_CTOR = null;
JDBC_4_PSTMT_3_ARG_CTOR = null;
JDBC_4_PSTMT_4_ARG_CTOR = null;
}
}
//......
}
com.mysql.jdbc.PreparedStatement在static方法初始化了JDBC_4_PSTMT_3_ARG_CTOR,其构造器有三个参数,分别是MySQLConnection.class, String.class,String.class,使用的类是com.mysql.jdbc.JDBC4PreparedStatement
JDBC4PreparedStatement
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/JDBC4PreparedStatement.java
public class JDBC4PreparedStatement extends PreparedStatement {
public JDBC4PreparedStatement(MySQLConnection conn, String catalog) throws SQLException {
super(conn, catalog);
}
public JDBC4PreparedStatement(MySQLConnection conn, String sql, String catalog)
throws SQLException {
super(conn, sql, catalog);
}
public JDBC4PreparedStatement(MySQLConnection conn, String sql, String catalog,
ParseInfo cachedParseInfo) throws SQLException {
super(conn, sql, catalog, cachedParseInfo);
}
public void setRowId(int parameterIndex, RowId x) throws SQLException {
JDBC4PreparedStatementHelper.setRowId(this, parameterIndex, x);
}
/**
* JDBC 4.0 Set a NCLOB parameter.
*
* @param i
* the first parameter is 1, the second is 2, ...
* @param x
* an object representing a NCLOB
*
* @throws SQLException
* if a database error occurs
*/
public void setNClob(int parameterIndex, NClob value) throws SQLException {
JDBC4PreparedStatementHelper.setNClob(this, parameterIndex, value);
}
public void setSQLXML(int parameterIndex, SQLXML xmlObject)
throws SQLException {
JDBC4PreparedStatementHelper.setSQLXML(this, parameterIndex, xmlObject);
}
}
JDBC4PreparedStatement的三个参数构造器主要是调用了父类PreparedStatement的对应的构造器;JDBC4PreparedStatement主要是支持了setNClob、setSQLXML
new PreparedStatement
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/PreparedStatement.java
/**
* Constructor for the PreparedStatement class.
*
* @param conn
* the connection creating this statement
* @param sql
* the SQL for this statement
* @param catalog
* the catalog/database this statement should be issued against
*
* @throws SQLException
* if a database error occurs.
*/
public PreparedStatement(MySQLConnection conn, String sql, String catalog)
throws SQLException {
super(conn, catalog);
if (sql == null) {
throw SQLError.createSQLException(Messages.getString("PreparedStatement.0"), //$NON-NLS-1$
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
detectFractionalSecondsSupport();
this.originalSql = sql;
if (this.originalSql.startsWith(PING_MARKER)) {
this.doPingInstead = true;
} else {
this.doPingInstead = false;
}
this.dbmd = this.connection.getMetaData();
this.useTrueBoolean = this.connection.versionMeetsMinimum(3, 21, 23);
this.parseInfo = new ParseInfo(sql, this.connection, this.dbmd,
this.charEncoding, this.charConverter);
initializeFromParseInfo();
this.compensateForOnDuplicateKeyUpdate = this.connection.getCompensateOnDuplicateKeyUpdateCounts();
if (conn.getRequiresEscapingEncoder())
charsetEncoder = Charset.forName(conn.getEncoding()).newEncoder();
}
这里主要是用了connection的metaData、以及构造ParseInfo
StatementImpl
mysql-connector-java-5.1.21-sources.jar!/com/mysql/jdbc/StatementImpl.java
public StatementImpl(MySQLConnection c, String catalog) throws SQLException {
if ((c == null) || c.isClosed()) {
throw SQLError.createSQLException(
Messages.getString("Statement.0"), //$NON-NLS-1$
SQLError.SQL_STATE_CONNECTION_NOT_OPEN, null); //$NON-NLS-1$ //$NON-NLS-2$
}
this.connection = c;
this.connectionId = this.connection.getId();
this.exceptionInterceptor = this.connection
.getExceptionInterceptor();
this.currentCatalog = catalog;
this.pedantic = this.connection.getPedantic();
this.continueBatchOnError = this.connection.getContinueBatchOnError();
this.useLegacyDatetimeCode = this.connection.getUseLegacyDatetimeCode();
if (!this.connection.getDontTrackOpenResources()) {
this.connection.registerStatement(this);
}
//
// Adjust, if we know it
//
if (this.connection != null) {
this.maxFieldSize = this.connection.getMaxAllowedPacket();
int defaultFetchSize = this.connection.getDefaultFetchSize();
if (defaultFetchSize != 0) {
setFetchSize(defaultFetchSize);
}
if (this.connection.getUseUnicode()) {
this.charEncoding = this.connection.getEncoding();
this.charConverter = this.connection.getCharsetConverter(this.charEncoding);
}
boolean profiling = this.connection.getProfileSql()
|| this.connection.getUseUsageAdvisor() || this.connection.getLogSlowQueries();
if (this.connection.getAutoGenerateTestcaseScript() || profiling) {
this.statementId = statementCounter++;
}
if (profiling) {
this.pointOfOrigin = new Throwable();
this.profileSQL = this.connection.getProfileSql();
this.useUsageAdvisor = this.connection.getUseUsageAdvisor();
this.eventSink = ProfilerEventHandlerFactory.getInstance(this.connection);
}
int maxRowsConn = this.connection.getMaxRows();
if (maxRowsConn != -1) {
setMaxRows(maxRowsConn);
}
this.holdResultsOpenOverClose = this.connection.getHoldResultsOpenOverStatementClose();
}
version5013OrNewer = this.connection.versionMeetsMinimum(5, 0, 13);
}
这里会获取connection的一系列配置,同时对于需要trackOpenResources的会执行registerStatement(这个在realClose的时候会unregister
)
参数值
isJdbc4
com/mysql/jdbc/Util.java
private static boolean isJdbc4 = false;
try {
Class.forName("java.sql.NClob");
isJdbc4 = true;
} catch (Throwable t) {
isJdbc4 = false;
}
isJdbc4默认为false,在检测到java.sql.NClob类的时候为true;jdk8版本支持jdbc4
useServerPreparedStmts
com/mysql/jdbc/ConnectionPropertiesImpl.java
private BooleanConnectionProperty detectServerPreparedStmts = new BooleanConnectionProperty(
"useServerPrepStmts", //$NON-NLS-1$
false,
Messages.getString("ConnectionProperties.useServerPrepStmts"), //$NON-NLS-1$
"3.1.0", MISC_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$
useServerPreparedStmts默认为false
cachePrepStmts
com/mysql/jdbc/ConnectionPropertiesImpl.java
private BooleanConnectionProperty cachePreparedStatements = new BooleanConnectionProperty(
"cachePrepStmts", //$NON-NLS-1$
false,
Messages.getString("ConnectionProperties.cachePrepStmts"), //$NON-NLS-1$
"3.0.10", PERFORMANCE_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$
cachePrepStmts默认为false
小结
- mysql的jdbc driver的prepareStatement首先根据useServerPreparedStmts以及getEmulateUnsupportedPstmts来判断是否要通过canHandleAsServerPreparedStatement判断canServerPrepare;之后在useServerPreparedStmts及canServerPrepare为true时,根据cachePreparedStatements做ServerPreparedStatement的处理;如果不开启serverPrepare则执行clientPrepareStatement(
useServerPreparedStmts及cachePrepStmts参数默认为false
) - clientPrepareStatement在cachePreparedStatements为true时会从cachedPreparedStatementParams(
缓存的key为nativeSql,value为ParseInfo
)去获取ParseInfo,获取不到则执行com.mysql.jdbc.PreparedStatement.getInstance再放入缓存,获取到ParseInfo则通过com.mysql.jdbc.PreparedStatement(getLoadBalanceSafeProxy(), nativeSql,this.database, pStmtInfo)创建PreparedStatement;如果为false则直接通过com.mysql.jdbc.PreparedStatement.getInstance来创建 - useServerPreparedStmts为true时,创建的是ServerPreparedStatement(
创建的时候会触发prepare操作,往mysql服务端发送COM_PREPARE指令
),本地通过serverSideStatementCache类来缓存ServerPreparedStatement,key为sql
doc
- 预编译语句(Prepared Statements)介绍,以MySQL为例