前言
最近聽一個老師講了公開課,在其中講到了PreparedStatement的執(zhí)行原理和Statement的區(qū)別。
當時聽公開課老師講的時候感覺以前就只知道PreparedStatement是“預編譯類”,能夠?qū)ql語句進行預編譯,預編譯后能夠提高數(shù)據(jù)庫sql語句執(zhí)行效率。
但是,聽了那個老師講后我就突然很想問自己,預編譯??是誰對sql語句的預編譯??是數(shù)據(jù)庫?還是PreparedStatement對象??到底什么是預編譯??為什么能夠提高效率??為什么在數(shù)據(jù)庫操作時能夠防止sql注入攻擊??這就引起了我對Preparedstatement的疑惑。
公開課老師講的時候說:”PreparedStatement會對sql文進行預編譯,預編譯后,會存儲在PreparedStatement對象中,等下次再執(zhí)行這個PreparedStatement對象時,會提高很多效率”。這句話我聽了后更疑惑了,預編譯是什么我不知道就算了,竟然還說:對sql預編譯后會存儲在PreparedStatement對象中??我就想問問sql預編譯后是什么??什么被存儲在PreparedStatement對象中??
更讓人感覺疑惑的是Statement。對就是Statement,公開課老師說:“同一條sql語句(字符串都是相同的)在Statement對象中多次執(zhí)行時,Statement只會對當前sql文編譯一次,編譯后存儲在Statement中,在之后的執(zhí)行過程中,都不會進行編譯而是直接運行sql語句”。什么??我沒聽錯吧?Statement還有編譯??等等等等。。。。我當時真的是聽的懷疑人生。
PreparedStatement
在說PreparedStatement之前,我們來看看什么是預編譯。其實預編譯是MySQL數(shù)據(jù)庫本身都支持的。但是MySQL Server 4.1之前的版本是不支持預編譯的。(具體是否包括4.1還得讀者們親自試驗)
在這里,筆者用的是MySQL5.6綠色版。
MySQL中的預編譯功能是這樣的
預編譯的好處:
大家平時都使用過JDBC中的PreparedStatement接口,它有預編譯功能。什么是預編譯功能呢?它有什么好處呢?
當客戶發(fā)送一條SQL語句給服務器后,服務器總是需要校驗SQL語句的語法格式是否正確,然后把SQL語句編譯成可執(zhí)行的函數(shù),最后才是執(zhí)行SQL語句。其中校驗語法,和編譯所花的時間可能比執(zhí)行SQL語句花的時間還要多。
注意:可執(zhí)行函數(shù)存儲在MySQL服務器中,并且當前連接斷開后,MySQL服務器會清除已經(jīng)存儲的可執(zhí)行函數(shù)。
如果我們需要執(zhí)行多次insert語句,但只是每次插入的值不同,MySQL服務器也是需要每次都去校驗SQL語句的語法格式,以及編譯,這就浪費了太多的時間。如果使用預編譯功能,那么只對SQL語句進行一次語法校驗和編譯,所以效率要高。
1
2
3
4
MySQL執(zhí)行預編譯
MySQL執(zhí)行預編譯分為如三步:
1.執(zhí)行預編譯語句,例如:prepare showUsersByLikeName from 'select * from user where username like ?';
2.設(shè)置變量,例如:set @username='%小明%';
3.執(zhí)行語句,例如:execute showUsersByLikeName using @username;
1
2
3
如果需要再次執(zhí)行myfun,那么就不再需要第一步,即不需要再編譯語句了:
1.設(shè)置變量,例如:set @username='%小宋%';
2.執(zhí)行語句,例如:execute showUsersByLikeName using @username;
1
2
如果你看MySQL日志記錄,你就會看到:
配置MySQL日志記錄
路徑地址可以自己修改。
log-output=FILE
general-log=1
general_log_file="E:\mysql.log"
slow-query-log=1
slow_query_log_file="E:\mysql_slow.log"
long_query_time=2
1
2
3
4
5
6
配置之后就重啟MySQL服務器:
在cmd管理員界面執(zhí)行以下操作。
net stop mysql
net start mysql
1
2
使用PreparedStatement執(zhí)行sql查詢
JDBC MySQL驅(qū)動5.0.5以后的版本默認PreparedStatement是關(guān)閉預編譯功能的,所以需要我們手動開啟。而之前的JDBC MySQL驅(qū)動版本默認是開啟預編譯功能的。
MySQL數(shù)據(jù)庫服務器的預編譯功能在4.1之后才支持預編譯功能的。如果數(shù)據(jù)庫服務器不支持預編譯功能時,并且使用PreparedStatement開啟預編譯功能是會拋出異常的。這點非常重要。筆者用的是mysql-connector-jar-5.1.13版本的JDBC驅(qū)動。
在我們以前寫項目的時候,貌似都沒有注意是否開啟PreparedStatement的預編譯功能,以為它一直都是在使用的,現(xiàn)在看看不開啟PreparedStatement的預編譯,查看MySQL的日志輸出到底是怎么樣的。
@Test
public void showUser(){
//數(shù)據(jù)庫連接
Connection connection = null;
//預編譯的Statement,使用預編譯的Statement提高數(shù)據(jù)庫性能
PreparedStatement preparedStatement = null;
//結(jié)果 集
ResultSet resultSet = null;
try {
//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.jdbc.Driver");
//通過驅(qū)動管理類獲取數(shù)據(jù)庫鏈接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "");
//定義sql語句 ?表示占位符
String sql = "select * from user where username = ?";
//獲取預處理statement
preparedStatement = connection.prepareStatement(sql);
//設(shè)置參數(shù),第一個參數(shù)為sql語句中參數(shù)的序號(從1開始),第二個參數(shù)為設(shè)置的參數(shù)值
preparedStatement.setString(1, "王五");
//向數(shù)據(jù)庫發(fā)出sql執(zhí)行查詢,查詢出結(jié)果集
resultSet = preparedStatement.executeQuery();
preparedStatement.setString(1, "張三");
resultSet = preparedStatement.executeQuery();
//遍歷查詢結(jié)果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
resultSet.close();
preparedStatement.close();
System.out.println("#############################");
} catch (Exception e) {
e.printStackTrace();
}finally{
//釋放資源
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
這是輸出日志:
20 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment
20 Query SHOW COLLATION
20 Query SET NAMES utf8mb4
20 Query SET character_set_results = NULL
20 Query SET autocommit=1
20 Query select * from user where username = '王五'
20 Query select * from user where username = '張三'
20 Quit
1
2
3
4
5
6
7
8
可以看到,在日志中并沒有看到"prepare"命令來預編譯"select * from user where username = ?"這個sql模板。所以我們一般用的PreparedStatement并沒有用到預編譯功能的,只是用到了防止sql注入攻擊的功能。防止sql注入攻擊的實現(xiàn)是在PreparedStatement中實現(xiàn)的,和服務器無關(guān)。筆者在源碼中看到,PreparedStatement對敏感字符已經(jīng)轉(zhuǎn)義過了。
在PreparedStatement中開啟預編譯功能
設(shè)置MySQL連接URL參數(shù):useServerPrepStmts=true,如下所示。
jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true
這樣才能保證mysql驅(qū)動會先把SQL語句發(fā)送給服務器進行預編譯,然后在執(zhí)行executeQuery()時只是把參數(shù)發(fā)送給服務器。
再次執(zhí)行上面的程序看下MySQL日志輸出:
21 Query SHOW WARNINGS
21 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment
21 Query SHOW COLLATION
21 Query SET NAMES utf8mb4
21 Query SET character_set_results = NULL
21 Query SET autocommit=1
21 Prepare select * from user where username = ?
21 Execute select * from user where username = '王五'
21 Execute select * from user where username = '張三'
21 Close stmt
21 Quit
1
2
3
4
5
6
7
8
9
10
11
很明顯已經(jīng)進行了預編譯,Prepare select * from user where username = ?,這一句就是對sql語句模板進行預編譯的日志。好的非常Nice。
注意:
我們設(shè)置的是MySQL連接參數(shù),目的是告訴MySQL JDBC的PreparedStatement使用預編譯功能(5.0.5之后的JDBC驅(qū)動版本需要手動開啟,而之前的默認是開啟的),不管我們是否使用預編譯功能,MySQL Server4.1版本以后都是支持預編譯功能的。
cachePrepStmts參數(shù)
當使用不同的PreparedStatement對象來執(zhí)行相同的SQL語句時,還是會出現(xiàn)編譯兩次的現(xiàn)象,這是因為驅(qū)動沒有緩存編譯后的函數(shù)key,導致二次編譯。如果希望緩存編譯后函數(shù)的key,那么就要設(shè)置cachePrepStmts參數(shù)為true。例如:
jdbc:mysql://localhost:3306/mybatis?useServerPrepStmts=true&cachePrepStmts=true
1
程序代碼:
@Test
public void showUser(){
//數(shù)據(jù)庫連接
Connection connection = null;
//預編譯的Statement,使用預編譯的Statement提高數(shù)據(jù)庫性能
PreparedStatement preparedStatement = null;
//結(jié)果 集
ResultSet resultSet = null;
try {
//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.jdbc.Driver");
//通過驅(qū)動管理類獲取數(shù)據(jù)庫鏈接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true", "root", "");
preparedStatement=connection.prepareStatement("select * from user where username like ?");
preparedStatement.setString(1, "%小明%");
resultSet = preparedStatement.executeQuery();
//遍歷查詢結(jié)果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
//注意這里必須要關(guān)閉當前PreparedStatement對象流,否則下次再次創(chuàng)建PreparedStatement對象的時候還是會再次預編譯sql模板,使用PreparedStatement對象后不關(guān)閉當前PreparedStatement對象流是不會緩存預編譯后的函數(shù)key的
resultSet.close();
preparedStatement.close();
preparedStatement=connection.prepareStatement("select * from user where username like ?");
preparedStatement.setString(1, "%三%");
resultSet = preparedStatement.executeQuery();
//遍歷查詢結(jié)果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
resultSet.close();
preparedStatement.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
//釋放資源
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
日志輸出:
24 Query SHOW WARNINGS
24 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment
24 Query SHOW COLLATION
24 Query SET NAMES utf8mb4
24 Query SET character_set_results = NULL
24 Query SET autocommit=1
24 Prepare select * from user where username like ?
24 Execute select * from user where username like '%小明%'
24 Execute select * from user where username like '%三%'
24 Quit
1
2
3
4
5
6
7
8
9
10
注意:每次使用PreparedStatement對象后都要關(guān)閉該PreparedStatement對象流,否則預編譯后的函數(shù)key是不會緩存的。
Statement執(zhí)行sql語句是否會對編譯后的函數(shù)進行緩存
這個不好說,對于每個數(shù)據(jù)庫的具體實現(xiàn)都是不一樣的,對于預編譯肯定都大體相同,但是對于Statement和普通sql,數(shù)據(jù)庫一般都是先檢查sql語句是否正確,然后編譯sql語句成為函數(shù),最后執(zhí)行函數(shù)。其實也不乏某些數(shù)據(jù)庫很瘋狂,對于普通sql的函數(shù)進行緩存。但是目前的主流數(shù)據(jù)庫都不會對sql函數(shù)進行緩存的。因為sql語句變化那么多,如果對所有函數(shù)緩存,那么對于內(nèi)存的消耗也是非常巨大的。
如果你不確定普通sql語句的函數(shù)是否被存儲,那要怎么做呢??
其實還是一個道理,查看MySQL日志記錄:檢查第二次執(zhí)行相同sql語句時,是否是直接通過execute來進行查詢的。
@Test
public void showUser(){
//數(shù)據(jù)庫連接
Connection connection = null;
//預編譯的Statement,使用預編譯的Statement提高數(shù)據(jù)庫性能
PreparedStatement preparedStatement = null;
//結(jié)果 集
ResultSet resultSet = null;
try {
//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.jdbc.Driver");
//通過驅(qū)動管理類獲取數(shù)據(jù)庫鏈接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true", "root", "");
Statement statement=connection.createStatement();
resultSet = statement.executeQuery("select * from user where username='小天'");
//遍歷查詢結(jié)果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
resultSet.close();
statement.close();
statement=connection.createStatement();
resultSet = statement.executeQuery("select * from user where username='小天'");
//遍歷查詢結(jié)果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
resultSet.close();
statement.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
//釋放資源
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
日志記錄:
26 Query SHOW WARNINGS
26 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment
26 Query SHOW COLLATION
26 Query SET NAMES utf8mb4
26 Query SET character_set_results = NULL
26 Query SET autocommit=1
26 Query select * from user where username='小天'
26 Query select * from user where username='小天'
26 Quit
1
2
3
4
5
6
7
8
9
看日志就會知道,都是Query命令,所以并沒有存儲函數(shù)。
總結(jié):
所以到了這里我的疑惑都解開了,PreparedStatement的預編譯是數(shù)據(jù)庫進行的,編譯后的函數(shù)key是緩存在PreparedStatement中的,編譯后的函數(shù)是緩存在數(shù)據(jù)庫服務器中的。預編譯前有檢查sql語句語法是否正確的操作。只有數(shù)據(jù)庫服務器支持預編譯功能時,JDBC驅(qū)動才能夠使用數(shù)據(jù)庫的預編譯功能,否則會報錯。預編譯在比較新的JDBC驅(qū)動版本中默認是關(guān)閉的,需要配置連接參數(shù)才能夠打開。在已經(jīng)配置好了數(shù)據(jù)庫連接參數(shù)的情況下,Statement對于MySQL數(shù)據(jù)庫是不會對編譯后的函數(shù)進行緩存的,數(shù)據(jù)庫不會緩存函數(shù),Statement也不會緩存函數(shù)的key,所以多次執(zhí)行相同的一條sql語句的時候,還是會先檢查sql語句語法是否正確,然后編譯sql語句成函數(shù),最后執(zhí)行函數(shù)。
對于PreparedStatement在設(shè)置參數(shù)的時候會對參數(shù)進行轉(zhuǎn)義處理。
因為PreparedStatement已經(jīng)對sql模板進行了編譯,并且存儲了函數(shù),所以PreparedStatement做的就是把參數(shù)進行轉(zhuǎn)義后直接傳入?yún)?shù)到數(shù)據(jù)庫,然后讓函數(shù)執(zhí)行。這就是為什么PreparedStatement能夠防止sql注入攻擊的原因了。
PreparedStatement的預編譯還有注意的問題,在數(shù)據(jù)庫端存儲的函數(shù)和在PreparedStatement中存儲的key值,都是建立在數(shù)據(jù)庫連接的基礎(chǔ)上的,如果當前數(shù)據(jù)庫連接斷開了,數(shù)據(jù)庫端的函數(shù)會清空,建立在連接上的PreparedStatement里面的函數(shù)key也會被清空,各個連接之間的預編譯都是互相獨立的。
使用Statement執(zhí)行預編譯
使用Statement執(zhí)行預編譯就是把上面的原始SQL語句預編譯執(zhí)行一次。
Connection con = JdbcUtils.getConnection();
Statement stmt = con.createStatement();
stmt.executeUpdate("prepare myfun from 'select * from t_book where bid=?'");
stmt.executeUpdate("set @str='b1'");
ResultSet rs = stmt.executeQuery("execute myfun using @str");
while(rs.next()) {
System.out.print(rs.getString(1) + ", ");
System.out.print(rs.getString(2) + ", ");
System.out.print(rs.getString(3) + ", ");
System.out.println(rs.getString(4));
}
stmt.executeUpdate("set @str='b2'");
rs = stmt.executeQuery("execute myfun using @str");
while(rs.next()) {
System.out.print(rs.getString(1) + ", ");
System.out.print(rs.getString(2) + ", ");
System.out.print(rs.getString(3) + ", ");
System.out.println(rs.getString(4));
}
rs.close();
stmt.close();
con.close();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在持久層框架中存在的問題
很多主流持久層框架(MyBatis,Hibernate)其實都沒有真正的用上預編譯,預編譯是要我們自己在參數(shù)列表上面配置的,如果我們不手動開啟,JDBC驅(qū)動程序5.0.5以后版本 默認預編譯都是關(guān)閉的。
所以我們要在參數(shù)列表中配置,例如:
jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true
1
注意:
在MySQL中,既要開啟預編譯也要開啟緩存。因為如果只是開啟預編譯的話效率還沒有不開啟預編譯效率高,大家可以做一下性能測試,其中性能測試結(jié)果在這篇博客中有寫到,探究mysql預編譯,而在MySQL中開啟預編譯和開啟緩存,其中的查詢效率和不開啟預編譯和不開啟緩存的效率是持平的。這里用的測試類是PreparedStatement。
參考資料:
探究mysql預編譯
PreparedStatement是如何大幅度提高性能的
參考中文文檔下載:MySQL預編譯功能
在寫這篇文章的時候發(fā)生了很多讓人惱火的事情,比如網(wǎng)上很多的答案基本上都是錯誤的,竟然還有人說好??不知道就不要亂說,亂發(fā)表博客,誤人子弟!!