下面是個最簡單的使用jdbc取得數據的應用。在例子之后我將分成4步,分別是①取得連接,②創建PreparedStatement,③設置參數,④執行查詢,來分步分析這個過程。除了設置參數那一步之外,其他的我都畫了時序圖,如果不想看文字的話,可以對着時序圖 。文中的第4步是組裝MySQL協議並發送數據包的關鍵,而且在這部分的(b)環節,我對於PreparedStatement的應用有詳細的代碼注釋分析,建議大家關注一下。
-
Java代碼
-
public class DBHelper {
-
public static Connection getConnection() {
-
Connection conn = null;
-
try {
-
Class.forName("com.mysql.jdbc.Driver");
-
conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",
-
"root", "root");
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
return conn;
-
}
-
}
-
-
/*dao中的方法*/
-
public List getAllAdvs() {
-
-
Connection conn = null;
-
ResultSet rs = null;
-
PreparedStatement stmt = null;
-
String sql = "select * from adv where id = ?";
-
List advs = new ArrayList();
-
-
conn = DBHelper.getConnection();
-
if (conn != null) {
-
try {
-
stmt = conn.prepareStatement(sql);
-
stmt.setInt(1, new Integer(1));
-
rs = stmt.executeQuery();
-
-
if (rs != null) {
-
while (rs.next()) {
-
Adv adv = new Adv();
-
adv.setId(rs.getLong(1));
-
adv.setName(rs.getString(2));
-
adv.setDesc(rs.getString(3));
-
adv.setPicUrl(rs.getString(4));
-
-
advs.add(adv);
-
}
-
}
-
} catch (SQLException e) {
-
e.printStackTrace();
-
} finally {
-
try {
-
stmt.close();
-
conn.close();
-
} catch (SQLException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
return advs;
-
}
-
public class DBHelper {
-
public static Connection getConnection() {
-
Connection conn = null;
-
try {
-
Class.forName("com.mysql.jdbc.Driver");
-
conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",
-
"root", "root");
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
return conn;
-
}
-
}
-
-
/*dao中的方法*/
-
public List getAllAdvs() {
-
-
Connection conn = null;
-
ResultSet rs = null;
-
PreparedStatement stmt = null;
-
String sql = "select * from adv where id = ?";
-
List advs = new ArrayList();
-
-
conn = DBHelper.getConnection();
-
if (conn != null) {
-
try {
-
stmt = conn.prepareStatement(sql);
-
stmt.setInt(1, new Integer(1));
-
rs = stmt.executeQuery();
-
-
if (rs != null) {
-
while (rs.next()) {
-
Adv adv = new Adv();
-
adv.setId(rs.getLong(1));
-
adv.setName(rs.getString(2));
-
adv.setDesc(rs.getString(3));
-
adv.setPicUrl(rs.getString(4));
-
-
advs.add(adv);
-
}
-
}
-
} catch (SQLException e) {
-
e.printStackTrace();
-
} finally {
-
try {
-
stmt.close();
-
conn.close();
-
} catch (SQLException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
return advs;
-
}
1、首先我們看到要的到一個數據庫連接,得到數據庫連接這部分放在DBHelper類中的getConnection方法中實現。Class.forName("com.mysql.jdbc.Driver");用來加載mysql的jdbc驅動。
-
Java代碼
-
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
-
static {
-
try {
-
java.sql.DriverManager.registerDriver(new Driver());
-
} catch (SQLException E) {
-
throw new RuntimeException("Can't register driver!");
-
}
-
}
-
public Driver() throws SQLException {
-
}
-
}
-
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
-
static {
-
try {
-
java.sql.DriverManager.registerDriver(new Driver());
-
} catch (SQLException E) {
-
throw new RuntimeException("Can't register driver!");
-
}
-
}
-
public Driver() throws SQLException {
-
}
-
}
Mysql的Driver類實現了java.sql.Driver接口,任何數據庫提供商的驅動類都必須實現這個接口。在DriverManager類中使用的都是接口Driver類型的驅動,也就是說驅動的使用不依賴於具體的實現,這無疑給我們的使用帶來很大的方便。如果需要換用其他的數據庫的話,只需要把Class.forName()中的參數換掉就可以了,可以說是非常方便的。
在com.mysql.jdbc.Driver類中,除了構造方法,就是一個static的方法體,它調用了DriverManager的registerDriver()方法,這個方法會加載所有系統提供的驅動,並把它們都假如到具體的驅動類中,當然現在就是mysql的Driver。在這里我們第一次看到了DriverManager類,這個類中提供了jdbc連接的主要操作,創建連接就是在這里完成的,可以說這是一個管理驅動的工具類。
-
Java代碼
-
public static synchronized void registerDriver(java.sql.Driver driver)
-
throws SQLException {
-
if (!initialized) {
-
initialize();
-
}
-
DriverInfo di = new DriverInfo();
-
/*把driver的信息封裝一下,組成一個DriverInfo對象*/
-
di.driver = driver;
-
di.driverClass = driver.getClass();
-
di.driverClassName = di.driverClass.getName();
-
writeDrivers.addElement(di);
-
println("registerDriver: " + di);
-
readDrivers = (java.util.Vector) writeDrivers.clone();
-
}
-
public static synchronized void registerDriver(java.sql.Driver driver)
-
throws SQLException {
-
if (!initialized) {
-
initialize();
-
}
-
DriverInfo di = new DriverInfo();
-
/*把driver的信息封裝一下,組成一個DriverInfo對象*/
-
di.driver = driver;
-
di.driverClass = driver.getClass();
-
di.driverClassName = di.driverClass.getName();
-
-
writeDrivers.addElement(di);
-
println("registerDriver: " + di);
-
readDrivers = (java.util.Vector) writeDrivers.clone();
-
-
}
注冊驅動首先就是初始化,然后把驅動的信息封裝一下放進一個叫做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(),如下:
-
Java代碼
-
private static Connection getConnection(
-
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
-
java.util.Vector drivers = null;
-
...
-
if (!initialized) {
-
initialize();
-
}
-
/*取得連接使用的driver從readDrivers中取*/
-
synchronized (DriverManager.class){
-
drivers = readDrivers;
-
}
-
-
SQLException reason = null;
-
for (int i = 0; i < drivers.size(); i++) {
-
DriverInfo di = (DriverInfo)drivers.elementAt(i);
-
-
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
-
continue;
-
}
-
try {
-
/*找到可供使用的驅動,連接數據庫server*/
-
Connection result = di.driver.connect(url, info);
-
if (result != null) {
-
return (result);
-
}
-
} catch (SQLException ex) {
-
if (reason == null) {
-
reason = ex;
-
}
-
}
-
}
-
-
if (reason != null) {
-
println("getConnection failed: " + reason);
-
throw reason;
-
}
-
throw new SQLException("No suitable driver found for "+ url, "08001");
-
}
-
private static Connection getConnection(
-
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
-
java.util.Vector drivers = null;
-
...
-
if (!initialized) {
-
initialize();
-
}
-
/*取得連接使用的driver從readDrivers中取*/
-
synchronized (DriverManager.class){
-
drivers = readDrivers;
-
}
-
-
SQLException reason = null;
-
for (int i = 0; i < drivers.size(); i++) {
-
DriverInfo di = (DriverInfo)drivers.elementAt(i);
-
-
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
-
continue;
-
}
-
try {
-
/*找到可供使用的驅動,連接數據庫server*/
-
Connection result = di.driver.connect(url, info);
-
if (result != null) {
-
return (result);
-
}
-
} catch (SQLException ex) {
-
if (reason == null) {
-
reason = ex;
-
}
-
}
-
}
-
-
if (reason != null) {
-
println("getConnection failed: " + reason);
-
throw reason;
-
}
-
throw new SQLException("No suitable driver found for "+ url, "08001");
-
}
Initialize()簡直無所不在,DriverManager中只要使用driver之前,就要檢查一下有沒有初始化,非常小心。然后開始遍歷所有驅動,直到找到一個可用的驅動,用這個驅動來取得一個數據庫連接,最后返回這個連接。當然,這是正常的情況,從上面我們可以看到,程序中對異常的處理很仔細。如果連接失敗,會記錄拋出的第一個異常信息,如果沒有找到合適的驅動,就拋出一個08001的錯誤。
現在重點就是假如一切正常,就應該從driver.connect()返回一個數據庫連接,所以我們來看看如何通過url提供的數據庫。
-
Java代碼
-
public java.sql.Connection connect(String url, Properties info)
-
throws SQLException {
-
Properties props = null;
-
if ((props = parseURL(url, info)) == null) {
-
return null;
-
}
-
-
try {
-
Connection newConn = new com.mysql.jdbc.Connection(host(props),
-
port(props), props, database(props), url);
-
-
return newConn;
-
} catch (SQLException sqlEx) {
-
throw sqlEx;
-
} catch (Exception ex) {
-
throw SQLError.createSQLException(...);
-
}
-
}
-
public java.sql.Connection connect(String url, Properties info)
-
throws SQLException {
-
Properties props = null;
-
if ((props = parseURL(url, info)) == null) {
-
return null;
-
}
-
-
try {
-
Connection newConn = new com.mysql.jdbc.Connection(host(props),
-
port(props), props, database(props), url);
-
-
return newConn;
-
} catch (SQLException sqlEx) {
-
throw sqlEx;
-
} catch (Exception ex) {
-
throw SQLError.createSQLException(...);
-
}
-
}
很簡潔的寫法,就是新建了一個mysql的connection,host, port, database給它傳進入,讓它去連接就對了,props里面是些什么東西呢,就是把url拆解一下,什么host,什么數據庫名,然后url后面的一股腦的參數,再把用戶跟密碼也都放進入,反正就是所有的連接數據都放進入了。
在com.mysql.jdbc.Connection的構造方法里面,會先做一些連接的初始化操作,例如創建PreparedStatement的cache,創建日志等等。然后就進入createNewIO()來建立連接了。
從時序圖中可以看到,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中的參數常量主要有以下幾種:
-
TYPE_FORWARD_ONLY: ResultSet的游標只能向前移動。
-
TYPE_SCROLL_INSENSITIVE:ResultSet的游標可以滾動,但對於resultSet下的數據改變不敏感。
-
TYPE_SCROLL_SENSITIVE:ResultSet的游標可以滾動,但對於resultSet下的數據改變是敏感的。
-
CONCUR_READ_ONLY:不可以更新的ResultSet的並發模式。
-
CONCUR_UPDATABLE:可以更新的ResultSet的並發模式。
-
FETCH_FORWARD:按正向(即從第一個到最后一個)處理結果集中的行。
-
FETCH_REVERSE:按反向(即從最后一個到第一個)處理結果集中的行處理。
-
FETCH_UNKNOWN:結果集中的行的處理順序未知。
-
CLOSE_CURSORS_AT_COMMIT:調用Connection.commit方法時應該關閉 ResultSet 對
-
HOLD_CURSORS_OVER_COMMIT:調用Connection.commit方法時不應關閉ResultSet對象。
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語句的二進制流,請看下面的代碼分析。
-
Java代碼
-
protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
-
InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
-
int[] batchedStreamLengths) throws SQLException {
-
// 從connection的IO中得到發送數據包,首先清空其中的數據
-
Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
-
sendPacket.clear();
-
-
/* 數據包的第一位為一個操作標識符(MysqlDefs.QUERY),表示驅動向服務器發送的連接的操作信號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操作信號並不是針對sql語句操作而言的CRUD操作,從提供的幾種參數來看,這個操作是針對服務器的一個操作。一般而言,使用到的都是MysqlDefs.QUERY,表示發送的是要執行sql語句的操作。
-
*/
-
sendPacket.writeByte((byte) MysqlDefs.QUERY);
-
-
boolean useStreamLengths = this.connection
-
.getUseStreamLengthsInPrepStmts();
-
-
int ensurePacketSize = 0;
-
for (int i = 0; i < batchedParameterStrings.length; i++) {
-
if (batchedIsStream[i] && useStreamLengths) {
-
ensurePacketSize += batchedStreamLengths[i];
-
}
-
}
-
-
/* 判斷這個sendPacket的byte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer
-
*/
-
if (ensurePacketSize != 0) {
-
sendPacket.ensureCapacity(ensurePacketSize);
-
}
-
-
/* 遍歷所有的參數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過分割,如果含有以問號標識的參數占位符的話,就記錄下這個占位符的位置,依據這個位置把sql分割成多段,放在了一個名為staticSql的字符串中。這里就開始把sql語句進行拼裝,把staticSql和parameterValues進行組合,放在操作符的后面。
-
*/
-
for (int i = 0; i < batchedParameterStrings.length; i++) {
-
-
/* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果兩者都為null,說明在參數的設置過程中出了錯,立即拋出錯誤。
-
*/
-
if ((batchedParameterStrings[i] == null)
-
&& (batchedParameterStreams[i] == null)) {
-
throw SQLError.createSQLException(Messages
-
.getString("PreparedStatement.40") //$NON-NLS-1$
-
+ (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
-
}
-
-
/*在sendPacket中加入staticSql數組中的元素,就是分割出來的沒有”?”的sql語句,並把字符串轉換成byte。
-
*/
-
sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
-
-
/* batchedIsStream就是isStream,如果參數是通過CallableStatement傳遞進來的話,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到問號占的參數位置中去。
-
*/
-
if (batchedIsStream[i]) {
-
streamToBytes(sendPacket, batchedParameterStreams[i], true,
-
batchedStreamLengths[i], useStreamLengths);
-
} else {
-
-
/*否則的話,就用batchedParameterStrings,也就是parameterValues來填充參數位置。在循環中,這個操作是跟在staticSql后面的,因此就把第i個參數加到了第i個staticSql段中。參考前面的staticSql的例子,發現當循環結束的時候,原始sql語句最后一個”?”之前的sql語句就拼成了正確的語句了。
-
*/
-
sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
-
}
-
}
-
-
/*由於在原始的包含問號的sql語句中,在最后一個”?”后面可能還有order by等語句,因此staticSql數組中的元素個數一定比參數的個數多1,所以這里把staticSqlString中的最后一段sql語句放入sendPacket中。
-
*/
-
sendPacket
-
.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
-
-
return sendPacket;
-
}
-
protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
-
InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
-
int[] batchedStreamLengths) throws SQLException {
-
// 從connection的IO中得到發送數據包,首先清空其中的數據
-
Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
-
sendPacket.clear();
-
-
/* 數據包的第一位為一個操作標識符(MysqlDefs.QUERY),表示驅動向服務器發送的連接的操作信號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操作信號並不是針對sql語句操作而言的CRUD操作,從提供的幾種參數來看,這個操作是針對服務器的一個操作。一般而言,使用到的都是MysqlDefs.QUERY,表示發送的是要執行sql語句的操作。
-
*/
-
sendPacket.writeByte((byte) MysqlDefs.QUERY);
-
-
boolean useStreamLengths = this.connection
-
.getUseStreamLengthsInPrepStmts();
-
-
int ensurePacketSize = 0;
-
for (int i = 0; i < batchedParameterStrings.length; i++) {
-
if (batchedIsStream[i] && useStreamLengths) {
-
ensurePacketSize += batchedStreamLengths[i];
-
}
-
}
-
-
/* 判斷這個sendPacket的byte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer
-
*/
-
if (ensurePacketSize != 0) {
-
sendPacket.ensureCapacity(ensurePacketSize);
-
}
-
-
/* 遍歷所有的參數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過分割,如果含有以問號標識的參數占位符的話,就記錄下這個占位符的位置,依據這個位置把sql分割成多段,放在了一個名為staticSql的字符串中。這里就開始把sql語句進行拼裝,把staticSql和parameterValues進行組合,放在操作符的后面。
-
*/
-
for (int i = 0; i < batchedParameterStrings.length; i++) {
-
-
/* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果兩者都為null,說明在參數的設置過程中出了錯,立即拋出錯誤。
-
*/
-
if ((batchedParameterStrings[i] == null)
-
&& (batchedParameterStreams[i] == null)) {
-
throw SQLError.createSQLException(Messages
-
.getString("PreparedStatement.40") //$NON-NLS-1$
-
+ (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
-
}
-
-
/*在sendPacket中加入staticSql數組中的元素,就是分割出來的沒有”?”的sql語句,並把字符串轉換成byte。
-
*/
-
sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
-
-
/* batchedIsStream就是isStream,如果參數是通過CallableStatement傳遞進來的話,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到問號占的參數位置中去。
-
*/
-
if (batchedIsStream[i]) {
-
streamToBytes(sendPacket, batchedParameterStreams[i], true,
-
batchedStreamLengths[i], useStreamLengths);
-
} else {
-
-
/*否則的話,就用batchedParameterStrings,也就是parameterValues來填充參數位置。在循環中,這個操作是跟在staticSql后面的,因此就把第i個參數加到了第i個staticSql段中。參考前面的staticSql的例子,發現當循環結束的時候,原始sql語句最后一個”?”之前的sql語句就拼成了正確的語句了。
-
*/
-
sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
-
}
-
}
-
-
/*由於在原始的包含問號的sql語句中,在最后一個”?”后面可能還有order by等語句,因此staticSql數組中的元素個數一定比參數的個數多1,所以這里把staticSqlString中的最后一段sql語句放入sendPacket中。
-
*/
-
sendPacket
-
.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
-
-
return sendPacket;
-
}
假如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服務器接收數據並解析,執行查詢了。
-
Java代碼
-
while (len >= this.maxThreeBytes) {
-
this.packetSequence++;
-
/*設置包的開始位置*/
-
headerPacket.setPosition(0);
-
/*設置這個數據包的大小,splitSize=255 * 255 * 255*/
-
headerPacket.writeLongInt(splitSize);
-
/*設置數據包的序號*/
-
headerPacket.writeByte(this.packetSequence);
-
/*origPacketBytes就是sendPacket,所以這里就是把sendPacket中大小為255 * 255 * 255的數據放入headPacket中,headerPacketBytes是headPacket的byte buffer*/
-
System.arraycopy(origPacketBytes, originalPacketPos,
-
headerPacketBytes, 4, splitSize);
-
-
int packetLen = splitSize + HEADER_LENGTH;
-
if (!this.useCompression) {
-
this.mysqlOutput.write(headerPacketBytes, 0,
-
splitSize + HEADER_LENGTH);
-
this.mysqlOutput.flush();
-
} else {
-
Buffer packetToSend;
-
-
headerPacket.setPosition(0);
-
packetToSend = compressPacket(headerPacket, HEADER_LENGTH,
-
splitSize, HEADER_LENGTH);
-
packetLen = packetToSend.getPosition();
-
/*往IO的output stream中寫數據*/
-
this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
-
packetLen);
-
this.mysqlOutput.flush();
-
}
-
-
originalPacketPos += splitSize;
-
len -= splitSize;
-
}
-
while (len >= this.maxThreeBytes) {
-
this.packetSequence++;
-
/*設置包的開始位置*/
-
① headerPacket.setPosition(0);
-
/*設置這個數據包的大小,splitSize=255 * 255 * 255*/
-
headerPacket.writeLongInt(splitSize);
-
/*設置數據包的序號*/
-
headerPacket.writeByte(this.packetSequence);
-
/*origPacketBytes就是sendPacket,所以這里就是把sendPacket中大小為255 * 255 * 255的數據放入headPacket中,headerPacketBytes是headPacket的byte buffer*/
-
System.arraycopy(origPacketBytes, originalPacketPos,
-
headerPacketBytes, 4, splitSize);
-
-
int packetLen = splitSize + HEADER_LENGTH;
-
if (!this.useCompression) {
-
this.mysqlOutput.write(headerPacketBytes, 0,
-
splitSize + HEADER_LENGTH);
-
this.mysqlOutput.flush();
-
} else {
-
Buffer packetToSend;
-
-
headerPacket.setPosition(0);
-
packetToSend = compressPacket(headerPacket, HEADER_LENGTH,
-
splitSize, HEADER_LENGTH);
-
packetLen = packetToSend.getPosition();
-
/*往IO的output stream中寫數據*/
-
this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
-
packetLen);
-
this.mysqlOutput.flush();
-
}
-
-
originalPacketPos += splitSize;
-
len -= splitSize;
-
}
iii. 通過readAllResults方法讀取查詢結果。這個讀取的過程與發送過程相反,如果接收到的數據包有多個的話,通過IO不斷讀取,並根據第packet第4個位置上的序號來組裝這些packet。然后把讀到的數據組裝成resultSet中的rowData,這個結果就是我們要的查詢結果了。
結合下面的executeQuery的時序圖再理一下思路就更清楚了。
至此,把resultSet一步步的返回給dao,接下來的過程,就是從resultSet中取出rowData,組合成我們自己需要的對象數據了。
總結一下,經過這次對mysql驅動的探索,我發現了更多關於mysql的底層細節,對於以后分析問題解決問題有很大幫助,當然,這里面還有很多細節文中沒有寫。另外一個就是對於PreparedStatement有了重新的認識,有些東西往往都是想當然得出來的結論,真相還是要靠實踐來發現。