jdbc獲取數據具體過程


下面是個最簡單的使用jdbc取得數據的應用。在例子之后我將分成4步,分別是①取得連接,②創建PreparedStatement,③設置參數,④執行查詢,來分步分析這個過程。除了設置參數那一步之外,其他的我都畫了時序圖,如果不想看文字的話,可以對着時序圖 。文中的第4步是組裝MySQL協議並發送數據包的關鍵,而且在這部分的(b)環節,我對於PreparedStatement的應用有詳細的代碼注釋分析,建議大家關注一下。

 
        
  1. Java代碼   

  2. public class DBHelper {     

  3.     public static Connection getConnection() {     

  4.         Connection conn = null;     

  5.         try {     

  6.             Class.forName("com.mysql.jdbc.Driver");     

  7.             conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",     

  8.                     "root", "root");     

  9.         } catch (Exception e) {     

  10.             e.printStackTrace();     

  11.         }     

  12.         return conn;     

  13.     }     

  14. }     

  15.     

  16.         /*dao中的方法*/    

  17.     public List getAllAdvs() {     

  18.              

  19.         Connection conn = null;     

  20.         ResultSet rs = null;     

  21.         PreparedStatement stmt = null;     

  22.         String sql = "select * from adv where id = ?";     

  23.         List advs = new ArrayList();     

  24.     

  25.         conn = DBHelper.getConnection();     

  26.         if (conn != null) {     

  27.             try {     

  28.                 stmt = conn.prepareStatement(sql);     

  29.                                 stmt.setInt(1, new Integer(1));     

  30.                 rs = stmt.executeQuery();     

  31.     

  32.                 if (rs != null) {     

  33.                     while (rs.next()) {     

  34.                         Adv adv = new Adv();     

  35.                         adv.setId(rs.getLong(1));     

  36.                         adv.setName(rs.getString(2));     

  37.                         adv.setDesc(rs.getString(3));     

  38.                         adv.setPicUrl(rs.getString(4));     

  39.     

  40.                         advs.add(adv);     

  41.                     }     

  42.                 }     

  43.             } catch (SQLException e) {     

  44.                 e.printStackTrace();     

  45.             } finally {     

  46.                 try {     

  47.                     stmt.close();     

  48.                     conn.close();     

  49.                 } catch (SQLException e) {     

  50.                     e.printStackTrace();     

  51.                 }     

  52.             }     

  53.         }     

  54.         return advs;     

  55.     }    

  56. public class DBHelper {  

  57.  public static Connection getConnection() {  

  58.   Connection conn = null;  

  59.   try {  

  60.    Class.forName("com.mysql.jdbc.Driver");  

  61.    conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",  

  62.      "root", "root");  

  63.   } catch (Exception e) {  

  64.    e.printStackTrace();  

  65.   }  

  66.   return conn;  

  67.  }  

  68. }  

  69.  

  70.         /*dao中的方法*/ 

  71.  public List getAllAdvs() {  

  72.     

  73.   Connection conn = null;  

  74.   ResultSet rs = null;  

  75.   PreparedStatement stmt = null;  

  76.   String sql = "select * from adv where id = ?";  

  77.   List advs = new ArrayList();  

  78.  

  79.   conn = DBHelper.getConnection();  

  80.   if (conn != null) {  

  81.    try {  

  82.     stmt = conn.prepareStatement(sql);  

  83.                                 stmt.setInt(1, new Integer(1));  

  84.     rs = stmt.executeQuery();  

  85.  

  86.     if (rs != null) {  

  87.      while (rs.next()) {  

  88.       Adv adv = new Adv();  

  89.       adv.setId(rs.getLong(1));  

  90.       adv.setName(rs.getString(2));  

  91.       adv.setDesc(rs.getString(3));  

  92.       adv.setPicUrl(rs.getString(4));  

  93.  

  94.       advs.add(adv);  

  95.      }  

  96.     }  

  97.    } catch (SQLException e) {  

  98.     e.printStackTrace();  

  99.    } finally {  

  100.     try {  

  101.      stmt.close();  

  102.      conn.close();  

  103.     } catch (SQLException e) {  

  104.      e.printStackTrace();  

  105.     }  

  106.    }  

  107.   }  

  108.   return advs;  

  109.  } 

 
        

1、首先我們看到要的到一個數據庫連接,得到數據庫連接這部分放在DBHelper類中的getConnection方法中實現。Class.forName("com.mysql.jdbc.Driver");用來加載mysql的jdbc驅動。

 
        
  1. Java代碼   

  2. public class Driver extends NonRegisteringDriver implements java.sql.Driver {     

  3.     static {     

  4.         try {     

  5.             java.sql.DriverManager.registerDriver(new Driver());     

  6.         } catch (SQLException E) {     

  7.             throw new RuntimeException("Can't register driver!");     

  8.         }     

  9.     }     

  10.     public Driver() throws SQLException {     

  11.     }     

  12. }    

  13. public class Driver extends NonRegisteringDriver implements java.sql.Driver {  

  14.  static {  

  15.   try {  

  16.    java.sql.DriverManager.registerDriver(new Driver());  

  17.   } catch (SQLException E) {  

  18.    throw new RuntimeException("Can't register driver!");  

  19.   }  

  20.  }  

  21.  public Driver() throws SQLException {  

  22.  }  

 
        

Mysql的Driver類實現了java.sql.Driver接口,任何數據庫提供商的驅動類都必須實現這個接口。在DriverManager類中使用的都是接口Driver類型的驅動,也就是說驅動的使用不依賴於具體的實現,這無疑給我們的使用帶來很大的方便。如果需要換用其他的數據庫的話,只需要把Class.forName()中的參數換掉就可以了,可以說是非常方便的。

在com.mysql.jdbc.Driver類中,除了構造方法,就是一個static的方法體,它調用了DriverManager的registerDriver()方法,這個方法會加載所有系統提供的驅動,並把它們都假如到具體的驅動類中,當然現在就是mysql的Driver。在這里我們第一次看到了DriverManager類,這個類中提供了jdbc連接的主要操作,創建連接就是在這里完成的,可以說這是一個管理驅動的工具類。

 
        
  1. Java代碼   

  2.    public static synchronized void registerDriver(java.sql.Driver driver)     

  3. throws SQLException {     

  4. if (!initialized) {     

  5.     initialize();     

  6. }            

  7. DriverInfo di = new DriverInfo();      

  8.   /*把driver的信息封裝一下,組成一個DriverInfo對象*/    

  9. di.driver = driver;     

  10. di.driverClass = driver.getClass();     

  11. di.driverClassName = di.driverClass.getName();       

  12. writeDrivers.addElement(di);      

  13. println("registerDriver: " + di);      

  14. readDrivers = (java.util.Vector) writeDrivers.clone();      

  15.    }    

  16.     public static synchronized void registerDriver(java.sql.Driver driver)  

  17.  throws SQLException {  

  18.  if (!initialized) {  

  19.      initialize();  

  20.  }        

  21.  DriverInfo di = new DriverInfo();  

  22.    /*把driver的信息封裝一下,組成一個DriverInfo對象*/ 

  23.  di.driver = driver;  

  24.  di.driverClass = driver.getClass();  

  25.  di.driverClassName = di.driverClass.getName();  

  26.  

  27.  writeDrivers.addElement(di);   

  28.  println("registerDriver: " + di);  

  29.  readDrivers = (java.util.Vector) writeDrivers.clone();  

  30.  

  31.     } 

 
         
         
        

注冊驅動首先就是初始化,然后把驅動的信息封裝一下放進一個叫做DriverInfo的驅動信息類中,最后放入一個驅動的集合中。初始化工作主要是完成所有驅動的加載。  至於驅動的集合writeDrivers和readDrivers,很有趣的是,無論是registerDriver還是deregisterDriver,都是先對writeDrivers中的數據進行添加或者刪除,然后再把writeDrivers中的驅動都拷貝到readDrivers中,但每次取出driver卻從來不從writeDrivers中取,都是通過readDrivers來獲得。我認為可以這樣理解,writeDrivers只負責注冊driver與注銷driver,而readDrivers只負責提供可用的driver,只有當writeDrivers中准備好了驅動,這些驅動才是可以使用的,所以才能被copy至readDrivers中以備使用。這樣一來,對內的注冊注銷與對外的提供使用就分開來了。

第二步就要根據url和用戶名,密碼來獲得數據庫的連接了。url一般都是這樣的格式:jdbc:protocol://host_name:port/db_name?parameter_name=param_value。開頭部分的protocal是對應於不同的數據庫提供商的協議,例如mysql的就是mysql。

DriverManager中有重載了四個getConnection(),因為我們有用戶名和密碼,就把用戶和密碼存放在Properties中,最后進入終極getConnection(),如下:

 
        
  1. Java代碼   

  2.    private static Connection getConnection(     

  3. String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {     

  4. java.util.Vector drivers = null;     

  5. ...     

  6. if (!initialized) {     

  7.     initialize();     

  8. }     

  9.    /*取得連接使用的driver從readDrivers中取*/    

  10. synchronized (DriverManager.class){      

  11.     drivers = readDrivers;       

  12.        }     

  13.     

  14. SQLException reason = null;     

  15. for (int i = 0; i < drivers.size(); i++) {     

  16.     DriverInfo di = (DriverInfo)drivers.elementAt(i);     

  17.           

  18.     if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {     

  19.     continue;     

  20.     }     

  21.     try {     

  22.        /*找到可供使用的驅動,連接數據庫server*/    

  23.     Connection result = di.driver.connect(url, info);     

  24.     if (result != null) {     

  25.         return (result);     

  26.     }     

  27.     } catch (SQLException ex) {     

  28.     if (reason == null) {     

  29.         reason = ex;     

  30.     }     

  31.     }     

  32. }     

  33.         

  34. if (reason != null)    {     

  35.     println("getConnection failed: " + reason);     

  36.     throw reason;     

  37. }         

  38. throw new SQLException("No suitable driver found for "+ url, "08001");     

  39.    }    

  40.     private static Connection getConnection(  

  41.  String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {  

  42.  java.util.Vector drivers = null;  

  43.  ...  

  44.  if (!initialized) {  

  45.      initialize();  

  46.  }  

  47.     /*取得連接使用的driver從readDrivers中取*/ 

  48.  synchronized (DriverManager.class){   

  49.      drivers = readDrivers;    

  50.         }  

  51.  

  52.  SQLException reason = null;  

  53.  for (int i = 0; i < drivers.size(); i++) {  

  54.      DriverInfo di = (DriverInfo)drivers.elementAt(i);  

  55.         

  56.      if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {  

  57.   continue;  

  58.      }  

  59.      try {  

  60.         /*找到可供使用的驅動,連接數據庫server*/ 

  61.   Connection result = di.driver.connect(url, info);  

  62.   if (result != null) {  

  63.       return (result);  

  64.   }  

  65.      } catch (SQLException ex) {  

  66.   if (reason == null) {  

  67.       reason = ex;  

  68.   }  

  69.      }  

  70.  }  

  71.       

  72.  if (reason != null)    {  

  73.      println("getConnection failed: " + reason);  

  74.      throw reason;  

  75.  }      

  76.  throw new SQLException("No suitable driver found for "+ url, "08001");  

  77.     } 

 
        

Initialize()簡直無所不在,DriverManager中只要使用driver之前,就要檢查一下有沒有初始化,非常小心。然后開始遍歷所有驅動,直到找到一個可用的驅動,用這個驅動來取得一個數據庫連接,最后返回這個連接。當然,這是正常的情況,從上面我們可以看到,程序中對異常的處理很仔細。如果連接失敗,會記錄拋出的第一個異常信息,如果沒有找到合適的驅動,就拋出一個08001的錯誤。

現在重點就是假如一切正常,就應該從driver.connect()返回一個數據庫連接,所以我們來看看如何通過url提供的數據庫。

 
        
  1. Java代碼   

  2. public java.sql.Connection connect(String url, Properties info)     

  3.         throws SQLException {     

  4.     Properties props = null;     

  5.     if ((props = parseURL(url, info)) == null) {     

  6.         return null;     

  7.     }     

  8.     

  9.     try {     

  10.         Connection newConn = new com.mysql.jdbc.Connection(host(props),     

  11.                 port(props), props, database(props), url);     

  12.     

  13.         return newConn;     

  14.     } catch (SQLException sqlEx) {     

  15.         throw sqlEx;     

  16.     } catch (Exception ex) {     

  17.         throw SQLError.createSQLException(...);     

  18.     }     

  19. }    

  20.  public java.sql.Connection connect(String url, Properties info)  

  21.    throws SQLException {  

  22.   Properties props = null;  

  23.   if ((props = parseURL(url, info)) == null) {  

  24.    return null;  

  25.   }  

  26.  

  27.   try {  

  28.    Connection newConn = new com.mysql.jdbc.Connection(host(props),  

  29.      port(props), props, database(props), url);  

  30.  

  31.    return newConn;  

  32.   } catch (SQLException sqlEx) {  

  33.    throw sqlEx;  

  34.   } catch (Exception ex) {  

  35.    throw SQLError.createSQLException(...);  

  36.   }  

  37.  } 

 
        

很簡潔的寫法,就是新建了一個mysql的connection,host, port, database給它傳進入,讓它去連接就對了,props里面是些什么東西呢,就是把url拆解一下,什么host,什么數據庫名,然后url后面的一股腦的參數,再把用戶跟密碼也都放進入,反正就是所有的連接數據都放進入了。

在com.mysql.jdbc.Connection的構造方法里面,會先做一些連接的初始化操作,例如創建PreparedStatement的cache,創建日志等等。然后就進入createNewIO()來建立連接了。

新建了一個mysql的connection

從時序圖中可以看到,createNewIO()就是新建了一個com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory來創建一個socket。然后就由這個mySqlIO來與MySql服務器進行握手(doHandshake()),這個doHandshake主要用來初始化與Mysql server的連接,負責登陸服務器和處理連接錯誤。在其中會分析所連接的mysql server的版本,根據不同的版本以及是否使用SSL加密數據都有不同的處理方式,並把要傳輸給數據庫server的數據都放在一個叫做packet的buffer中,調用send()方法往outputStream中寫入要發送的數據。

2、PreparedStatement stmt = conn.prepareStatement(sql);使用得到的connection創建一個Statement。Statement有許多種,我們常用的就是PreparedStatement,用於執行預編譯好的SQL語句,CallableStatement用於調用數據庫的存儲過程。它們的繼承關系如下圖所示。

繼承關系

一旦有了一個statement,就可以通過執行statement.executeQuery()並通過ResultSet對象讀出查詢結果(如果查詢有返回結果的話)。

創建statement的方法一般都有重載,我們看下面的prepareStatement:

Java代碼  public java.sql.PreparedStatement prepareStatement(String sql)    throws SQLException {    return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY,    java.sql.ResultSet.CONCUR_READ_ONLY);    }    public java.sql.PreparedStatement prepareStatement(String sql,    int resultSetType, int resultSetConcurrency) throws SQLException;   public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); }

public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;

如果沒有指定resultSetType和resultSetConcurrency的話,會給它們默認設置一個值。  ResultSet中的參數常量主要有以下幾種: 

 
        
  1. TYPE_FORWARD_ONLY: ResultSet的游標只能向前移動。   

  2. TYPE_SCROLL_INSENSITIVE:ResultSet的游標可以滾動,但對於resultSet下的數據改變不敏感。   

  3. TYPE_SCROLL_SENSITIVE:ResultSet的游標可以滾動,但對於resultSet下的數據改變是敏感的。   

  4. CONCUR_READ_ONLY:不可以更新的ResultSet的並發模式。   

  5. CONCUR_UPDATABLE:可以更新的ResultSet的並發模式。   

  6. FETCH_FORWARD:按正向(即從第一個到最后一個)處理結果集中的行。   

  7. FETCH_REVERSE:按反向(即從最后一個到第一個)處理結果集中的行處理。   

  8. FETCH_UNKNOWN:結果集中的行的處理順序未知。   

  9. CLOSE_CURSORS_AT_COMMIT:調用Connection.commit方法時應該關閉 ResultSet 對   

  10. HOLD_CURSORS_OVER_COMMIT:調用Connection.commit方法時不應關閉ResultSet對象。 

 
        

prepareStatement的創建如下圖所示:

prepareStatement的創建

在new ParseInfo中,會對這個sql語句進行分析,例如看看這個sql是什么語句;有沒有limit條件語句,還有一個重要的工作,如果使用的是PreparedStatement來准備sql語句的話,會在這里把sql語句進行分解。我們知道PreparedStatement對象在實例化創建時就被設置了一個sql語句,使用PreparedStatement對象執行的sql語句在首次發送到數據庫時,sql語句就會被編譯,這樣當多次執行同一個sql語句時,mysql就不用每次都去編譯sql語句了。

這個sql語句如果包含參數的話,可以用問號(”?”)來為參數進行占位,而不需要立即為參數賦值,而在語句執行之前,必須通過適當的set***()來為問號處的參數賦值。New ParseInfo()中,包含了參數的sql語句就會被分解為多段,放在staticSql中,以便需要設置參數時定位參數的位置。假如sql語句為“select * from adv where id = ? and name = ?”的話,那么staticSql中的元素就是3個,staticSql[3]={ ”select * from adv where id = ”, ” and name = ” , ””}。注意數組中最后一個元素,在這個例子中是””,因為我的例子里面最后一個就是”?”,如果sql語句是這樣的“select * from adv where id = ? and name = ? order by id”的話,staticSql就變成是這樣的{ ”select * from adv where id = ”, ” and name = ” , ” order by id”}。

3、stmt.setInt(1, new Integer(1));

設置sql語句中的參數值。

對於參數而言,PreparedStatement中一共有四個變量來儲存它們,分別是

a) byte[][] parameterValues:參數轉換為byte后的值。

b) InputStream[] parameterStreams:只有在調用存儲過程batch(CallableStatement)的時候才會用到它,否則它的數組中的值設置為null。

c) boolean[] isStream:是否為stream的標志,如果調用的是preparedStatement,isStream數組中的值均為false,若調用的是CallableStatement,則均設置為true。

d) boolean[] isNull:標識參數是否為空,設置為false。

這四個變量的一維數組的大小都是一樣的,sql語句中有幾個待set的參數(幾個問號),一維的元素個數就是多大。  4、ResultSet rs = stmt.executeQuery(); 一切准備就緒,開始執行查詢羅!

a) 檢查preparedStatement是否已關閉,如果已關閉,拋出一個SQLError.SQL_STATE_CONNECTION_NOT_OPEN的錯誤。

b) fillSendPacket:創建數據包,其中包含了要發送到服務器的查詢。

這個sendPacket就是mysql驅動要發送給數據庫服務器的協議數據。一般來說,協議的數據格式有兩種,一種是二進制流的格式,還有一種是文本的格式。文本協議就是基本上人可以直接閱讀的協議,一般是用ascii字符集,也有用utf8格式的,優點是便於理解,讀起來方便,擴充容易,缺點就是解析的時候比較麻煩,而且占用的空間比較大,冗余的數據比較多。二進制格式話,就需要服務器與客戶端協議規定固定的數據結構,哪個位置放什么數據,如果單獨看協議內容的話,很難理解數據含義,優點就是數據量小,解析的時候只要根據固定位置的值就能知道具體標識什么意義。

在這里使用的是二進制流的格式,也就是說協議中的數據格式是固定的,而且都要轉換成二進制。格式為第一個byte標識操作信號,后面開始就是完整的sql語句的二進制流,請看下面的代碼分析。

 
        
  1. Java代碼   

  2.     protected Buffer fillSendPacket(byte[][] batchedParameterStrings,     

  3.             InputStream[] batchedParameterStreams, boolean[] batchedIsStream,     

  4.             int[] batchedStreamLengths) throws SQLException {     

  5.         // 從connection的IO中得到發送數據包,首先清空其中的數據     

  6.         Buffer sendPacket = this.connection.getIO().getSharedSendPacket();     

  7.         sendPacket.clear();     

  8.              

  9.         /* 數據包的第一位為一個操作標識符(MysqlDefs.QUERY),表示驅動向服務器發送的連接的操作信號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操作信號並不是針對sql語句操作而言的CRUD操作,從提供的幾種參數來看,這個操作是針對服務器的一個操作。一般而言,使用到的都是MysqlDefs.QUERY,表示發送的是要執行sql語句的操作。    

  10. */    

  11.         sendPacket.writeByte((byte) MysqlDefs.QUERY);     

  12.     

  13.         boolean useStreamLengths = this.connection     

  14.                 .getUseStreamLengthsInPrepStmts();     

  15.     

  16.         int ensurePacketSize = 0;     

  17.         for (int i = 0; i < batchedParameterStrings.length; i++) {     

  18.             if (batchedIsStream[i] && useStreamLengths) {     

  19.                 ensurePacketSize += batchedStreamLengths[i];     

  20.             }     

  21.         }     

  22.     

  23.         /* 判斷這個sendPacket的byte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer    

  24. */    

  25.         if (ensurePacketSize != 0) {     

  26.             sendPacket.ensureCapacity(ensurePacketSize);     

  27.         }     

  28.     

  29.         /* 遍歷所有的參數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過分割,如果含有以問號標識的參數占位符的話,就記錄下這個占位符的位置,依據這個位置把sql分割成多段,放在了一個名為staticSql的字符串中。這里就開始把sql語句進行拼裝,把staticSql和parameterValues進行組合,放在操作符的后面。    

  30. */    

  31.         for (int i = 0; i < batchedParameterStrings.length; i++) {     

  32.     

  33.         /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果兩者都為null,說明在參數的設置過程中出了錯,立即拋出錯誤。    

  34. */    

  35.             if ((batchedParameterStrings[i] == null)     

  36.                     && (batchedParameterStreams[i] == null)) {     

  37.                 throw SQLError.createSQLException(Messages     

  38.                         .getString("PreparedStatement.40") //$NON-NLS-1$     

  39.                         + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);     

  40.             }     

  41.     

  42.         /*在sendPacket中加入staticSql數組中的元素,就是分割出來的沒有”?”的sql語句,並把字符串轉換成byte。    

  43. */    

  44.             sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);     

  45.     

  46.         /* batchedIsStream就是isStream,如果參數是通過CallableStatement傳遞進來的話,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到問號占的參數位置中去。    

  47. */    

  48.             if (batchedIsStream[i]) {     

  49.                 streamToBytes(sendPacket, batchedParameterStreams[i], true,     

  50.                         batchedStreamLengths[i], useStreamLengths);     

  51.             } else {     

  52.                  

  53.         /*否則的話,就用batchedParameterStrings,也就是parameterValues來填充參數位置。在循環中,這個操作是跟在staticSql后面的,因此就把第i個參數加到了第i個staticSql段中。參考前面的staticSql的例子,發現當循環結束的時候,原始sql語句最后一個”?”之前的sql語句就拼成了正確的語句了。    

  54. */    

  55.     sendPacket.writeBytesNoNull(batchedParameterStrings[i]);     

  56.             }     

  57.         }     

  58.     

  59.         /*由於在原始的包含問號的sql語句中,在最后一個”?”后面可能還有order by等語句,因此staticSql數組中的元素個數一定比參數的個數多1,所以這里把staticSqlString中的最后一段sql語句放入sendPacket中。    

  60. */    

  61.         sendPacket     

  62.                 .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);     

  63.     

  64.         return sendPacket;     

  65.     }    

  66.  protected Buffer fillSendPacket(byte[][] batchedParameterStrings,  

  67.    InputStream[] batchedParameterStreams, boolean[] batchedIsStream,  

  68.    int[] batchedStreamLengths) throws SQLException {  

  69.         // 從connection的IO中得到發送數據包,首先清空其中的數據  

  70.   Buffer sendPacket = this.connection.getIO().getSharedSendPacket();  

  71.   sendPacket.clear();  

  72.           

  73.         /* 數據包的第一位為一個操作標識符(MysqlDefs.QUERY),表示驅動向服務器發送的連接的操作信號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操作信號並不是針對sql語句操作而言的CRUD操作,從提供的幾種參數來看,這個操作是針對服務器的一個操作。一般而言,使用到的都是MysqlDefs.QUERY,表示發送的是要執行sql語句的操作。  

  74. */ 

  75.   sendPacket.writeByte((byte) MysqlDefs.QUERY);  

  76.  

  77.   boolean useStreamLengths = this.connection  

  78.     .getUseStreamLengthsInPrepStmts();  

  79.  

  80.   int ensurePacketSize = 0;  

  81.   for (int i = 0; i < batchedParameterStrings.length; i++) {  

  82.    if (batchedIsStream[i] && useStreamLengths) {  

  83.     ensurePacketSize += batchedStreamLengths[i];  

  84.    }  

  85.   }  

  86.  

  87.         /* 判斷這個sendPacket的byte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer  

  88. */ 

  89.   if (ensurePacketSize != 0) {  

  90.    sendPacket.ensureCapacity(ensurePacketSize);  

  91.   }  

  92.  

  93.         /* 遍歷所有的參數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過分割,如果含有以問號標識的參數占位符的話,就記錄下這個占位符的位置,依據這個位置把sql分割成多段,放在了一個名為staticSql的字符串中。這里就開始把sql語句進行拼裝,把staticSql和parameterValues進行組合,放在操作符的后面。  

  94. */ 

  95.   for (int i = 0; i < batchedParameterStrings.length; i++) {  

  96.  

  97.         /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果兩者都為null,說明在參數的設置過程中出了錯,立即拋出錯誤。  

  98. */ 

  99.    if ((batchedParameterStrings[i] == null)  

  100.      && (batchedParameterStreams[i] == null)) {  

  101.     throw SQLError.createSQLException(Messages  

  102.       .getString("PreparedStatement.40") //$NON-NLS-1$  

  103.       + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);  

  104.    }  

  105.  

  106.         /*在sendPacket中加入staticSql數組中的元素,就是分割出來的沒有”?”的sql語句,並把字符串轉換成byte。  

  107. */ 

  108.    sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);  

  109.  

  110.         /* batchedIsStream就是isStream,如果參數是通過CallableStatement傳遞進來的話,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到問號占的參數位置中去。  

  111. */ 

  112.    if (batchedIsStream[i]) {  

  113.     streamToBytes(sendPacket, batchedParameterStreams[i], true,  

  114.       batchedStreamLengths[i], useStreamLengths);  

  115.    } else {  

  116.      

  117.         /*否則的話,就用batchedParameterStrings,也就是parameterValues來填充參數位置。在循環中,這個操作是跟在staticSql后面的,因此就把第i個參數加到了第i個staticSql段中。參考前面的staticSql的例子,發現當循環結束的時候,原始sql語句最后一個”?”之前的sql語句就拼成了正確的語句了。  

  118. */ 

  119.  sendPacket.writeBytesNoNull(batchedParameterStrings[i]);  

  120.    }  

  121.   }  

  122.  

  123.         /*由於在原始的包含問號的sql語句中,在最后一個”?”后面可能還有order by等語句,因此staticSql數組中的元素個數一定比參數的個數多1,所以這里把staticSqlString中的最后一段sql語句放入sendPacket中。  

  124. */ 

  125.   sendPacket  

  126.     .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);  

  127.  

  128.   return sendPacket;  

  129.  } 

 
        

假如sql語句為“select * from adv where id = ?”的話,這個sendPacket中第一個byte的值就是3(MysqlDefs.QUERY的int值),后面接着的就是填充了參數值的完整的sql語句字符串(例如:select * from adv where id = 1)轉換成的byte格式。

於是,我們看到,好像sql語句在這里就已經不是帶”?”的preparedStatement,而是在驅動里面把參數替代到”?”中,再把完整的sql語句發送給mysql server來編譯,那么盡管只是參數改變,但對於mysql server來說,每次都是新的sql語句,都要進行編譯的。這與我們之前一直理解的PreparedStatement完全不一樣。照理來說,應該把帶”?”的sql語句發送給數據庫server,由mysql server來編譯這個帶”?”的sql語句,然后用實際的參數來替代”?”,這樣才是實現了sql語句只編譯一次的效果。sql語句預編譯的功能取決於server端,oracle就是支持sql預編譯的。

所以說,從mysql驅動的PreparedStatement里面,好像我們並沒有看到mysql支持預編譯功能的證據。(實際測試也表明,如果server沒有預編譯功能的話,PreparedStatement和Statement的效率幾乎一樣,甚至當使用次數不多的時候,PreparedStatement比Statement還要慢一些)。  但是並不是說PreparedStatement除了給我們帶來高效率就沒有其他作用了,它還有非常好的其他作用:  i. 極大的提高了sql語句的安全性,可以防止sql注入  ii. 代碼結構清晰,易於理解,便於維護。

2009-07-02增加(感謝gembler):其實,在mysql5上的版本是支持預編譯sql功能的。我用的驅動是5.0.6的,在com.mysql.jdbc.Connection中有一個參數useServerPreparedStmts,表明是否使用預編譯功能,所以如果把useServerPreparedStmts置為true的話,mysql驅動可以通過PreparedStatement的子類ServerPreparedStatement來實現真正的PreparedStatement的功能。在這個類的serverExecute方法里面,就負責告訴server,用現在提供的參數來動態綁定到編譯好的sql語句上。所以說,ServerPreparedStatement才是真正實現了所謂prepare statement。

c) 設置當前的數據庫名,並把之前的數據庫名記錄下來,在查詢完成之后還要恢復原狀。

d) 檢查一下之前是否有緩存的數據,如果不久之前執行過這個查詢,並且緩存了數據的話,就直接從緩存中取出。

e) 如果sql查詢沒有限制條件的話,為其設置默認的返回行數,若preparedStatement中已經設置了maxRows的話,就使用它。

f) executeInternal:執行查詢。  i. 設置當前數據庫連接,並調用connection的execSQL來執行查詢.然后繼續把要發送的查詢包,就是之間組裝完畢的sendPacket傳遞進入MysqlIO的sqlQueryDirect()。  ii. 接下來就要往server端發送我們的查詢指令啦(sendCommand),說到發送數據,不禁要問,如果這個待發送的數據包超級大,難道每次都是一次性的發送嗎?當然不是,如果數據包超過規定的最大值的話,就會把它分割一下,分成幾個不超過最大值的數據包來發送。  所以可以肯定,在分割的過程中,除了最后一個數據包,其他數據包的大小都是一樣的。那就這樣的數據包直接切割了進行發送的話,假如現在被分成了三個數據包,發送給mysql server,服務器怎么知道那個包是第一個呢,它讀數據該從什么地方開始讀呢,這都是問題,所以,我們要給每個數據包的前面加上一點屬性標志,這個標志一共占了4個byte。從代碼①處開始就是頭標識位的設置。第一位表示數據包的開始位置,就是數據存放的起始位置,一般都設置為0,就是從第一個位置開始。第二和第三個字節標識了這個數據包的大小,注意的是,這個大小是出去標識的4個字節的大小,對於非最后一個數據包來說,這個大小都是一樣的,就是splitSize,也就是maxThreeBytes,它的值是255 * 255 * 255。  最后一個字節中存放的就是數據包的編號了,從0開始遞增。  在標識位設置完畢之后,就可以把255 * 255 * 255大小的數據從我們准備好的待發送數據包中copy出來了,注意,前4位已經是標識位了,所以應該從第五個位置開始copy數據。  在數據包都裝配完畢之后,就可以往socket的outputSteam中發送數據了。接下來的事情,就是由mysql服務器接收數據並解析,執行查詢了。

 

 
        
  1. Java代碼   

  2. while (len >= this.maxThreeBytes) {     

  3.     this.packetSequence++;     

  4.     /*設置包的開始位置*/    

  5.    headerPacket.setPosition(0);      

  6.     /*設置這個數據包的大小,splitSize=255 * 255 * 255*/    

  7.     headerPacket.writeLongInt(splitSize);      

  8.     /*設置數據包的序號*/    

  9.     headerPacket.writeByte(this.packetSequence);      

  10.     /*origPacketBytes就是sendPacket,所以這里就是把sendPacket中大小為255 * 255 * 255的數據放入headPacket中,headerPacketBytes是headPacket的byte buffer*/    

  11.     System.arraycopy(origPacketBytes, originalPacketPos,     

  12.         headerPacketBytes, 4, splitSize);     

  13.     

  14.     int packetLen = splitSize + HEADER_LENGTH;     

  15.     if (!this.useCompression) {     

  16.         this.mysqlOutput.write(headerPacketBytes, 0,     

  17.             splitSize + HEADER_LENGTH);     

  18.         this.mysqlOutput.flush();     

  19.     } else {     

  20.         Buffer packetToSend;     

  21.     

  22.         headerPacket.setPosition(0);     

  23.         packetToSend = compressPacket(headerPacket, HEADER_LENGTH,     

  24.                 splitSize, HEADER_LENGTH);     

  25.         packetLen = packetToSend.getPosition();     

  26.         /*往IO的output stream中寫數據*/    

  27.         this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,     

  28.             packetLen);     

  29.         this.mysqlOutput.flush();     

  30.     }     

  31.     

  32.     originalPacketPos += splitSize;     

  33.     len -= splitSize;     

  34. }    

  35.             while (len >= this.maxThreeBytes) {  

  36.                 this.packetSequence++;  

  37.                 /*設置包的開始位置*/ 

  38.     ①          headerPacket.setPosition(0);   

  39.                 /*設置這個數據包的大小,splitSize=255 * 255 * 255*/ 

  40.                 headerPacket.writeLongInt(splitSize);   

  41.                 /*設置數據包的序號*/ 

  42.                 headerPacket.writeByte(this.packetSequence);   

  43.                 /*origPacketBytes就是sendPacket,所以這里就是把sendPacket中大小為255 * 255 * 255的數據放入headPacket中,headerPacketBytes是headPacket的byte buffer*/ 

  44.                 System.arraycopy(origPacketBytes, originalPacketPos,  

  45.                     headerPacketBytes, 4, splitSize);  

  46.  

  47.                 int packetLen = splitSize + HEADER_LENGTH;  

  48.                 if (!this.useCompression) {  

  49.                     this.mysqlOutput.write(headerPacketBytes, 0,  

  50.                         splitSize + HEADER_LENGTH);  

  51.                     this.mysqlOutput.flush();  

  52.                 } else {  

  53.                     Buffer packetToSend;  

  54.  

  55.                     headerPacket.setPosition(0);  

  56.                     packetToSend = compressPacket(headerPacket, HEADER_LENGTH,  

  57.                             splitSize, HEADER_LENGTH);  

  58.                     packetLen = packetToSend.getPosition();  

  59.                     /*往IO的output stream中寫數據*/ 

  60.                     this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,  

  61.                         packetLen);  

  62.                     this.mysqlOutput.flush();  

  63.                 }  

  64.  

  65.                 originalPacketPos += splitSize;  

  66.                 len -= splitSize;  

  67.             } 

 
        

iii. 通過readAllResults方法讀取查詢結果。這個讀取的過程與發送過程相反,如果接收到的數據包有多個的話,通過IO不斷讀取,並根據第packet第4個位置上的序號來組裝這些packet。然后把讀到的數據組裝成resultSet中的rowData,這個結果就是我們要的查詢結果了。

結合下面的executeQuery的時序圖再理一下思路就更清楚了。

executeQuery的時序圖

至此,把resultSet一步步的返回給dao,接下來的過程,就是從resultSet中取出rowData,組合成我們自己需要的對象數據了。

總結一下,經過這次對mysql驅動的探索,我發現了更多關於mysql的底層細節,對於以后分析問題解決問題有很大幫助,當然,這里面還有很多細節文中沒有寫。另外一個就是對於PreparedStatement有了重新的認識,有些東西往往都是想當然得出來的結論,真相還是要靠實踐來發現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM