序
本文主要研究一下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 necessaryif (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 necessaryif (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 CALLif (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 implementsjava.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为例