聊聊mysql jdbc的prepareStatement

本文主要研究一下mysql jdbc的prepareStatement

prepareStatement

java/sql/Connection.java

    /**
     * Creates a PreparedStatement object for sending
     * parameterized SQL statements to the database.
     * 

* 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. * *

Note: This method is optimized for handling * parametric SQL statements that benefit from precompilation. If * the driver supports precompilation, * the method prepareStatement 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 PreparedStatement * object is executed. This has no direct effect on users; however, it does * affect which methods throw certain SQLException objects. *

* Result sets created using the returned PreparedStatement * object will by default be type TYPE_FORWARD_ONLY * and have a concurrency level of CONCUR_READ_ONLY. * 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 PreparedStatement 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.
     * 

* Note: 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 *

*

* MySQL does not support precompilation of statements, so they are handled * by the driver. *

* * @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 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

你可能感兴趣的:(mysql)