JDBC:深入理解PreparedStatement和Statement

前言
最近听一个老师讲了公开课,在其中讲到了PreparedStatement的执行原理和Statement的区别。

当时听公开课老师讲的时候感觉以前就只知道PreparedStatement是“预编译类”,能够对sql语句进行预编译,预编译后能够提高数据库sql语句执行效率。

但是,听了那个老师讲后我就突然很想问自己,预编译??是谁对sql语句的预编译??是数据库?还是PreparedStatement对象??到底什么是预编译??为什么能够提高效率??为什么在数据库操作时能够防止sql注入攻击??这就引起了我对Preparedstatement的疑惑。

公开课老师讲的时候说:”PreparedStatement会对sql文进行预编译,预编译后,会存储在PreparedStatement对象中,等下次再执行这个PreparedStatement对象时,会提高很多效率”。这句话我听了后更疑惑了,预编译是什么我不知道就算了,竟然还说:对sql预编译后会存储在PreparedStatement对象中??我就想问问sql预编译后是什么??什么被存储在PreparedStatement对象中??

更让人感觉疑惑的是Statement。对就是Statement,公开课老师说:“同一条sql语句(字符串都是相同的)在Statement对象中多次执行时,Statement只会对当前sql文编译一次,编译后存储在Statement中,在之后的执行过程中,都不会进行编译而是直接运行sql语句”。什么??我没听错吧?Statement还有编译??等等等等。。。。我当时真的是听的怀疑人生。

PreparedStatement
在说PreparedStatement之前,我们来看看什么是预编译。其实预编译是MySQL数据库本身都支持的。但是MySQL Server 4.1之前的版本是不支持预编译的。(具体是否包括4.1还得读者们亲自试验)

在这里,笔者用的是MySQL5.6绿色版。

MySQL中的预编译功能是这样的
预编译的好处:

大家平时都使用过JDBC中的PreparedStatement接口,它有预编译功能。什么是预编译功能呢?它有什么好处呢?
当客户发送一条SQL语句给服务器后,服务器总是需要校验SQL语句的语法格式是否正确,然后把SQL语句编译成可执行的函数,最后才是执行SQL语句。其中校验语法,和编译所花的时间可能比执行SQL语句花的时间还要多。
注意:可执行函数存储在MySQL服务器中,并且当前连接断开后,MySQL服务器会清除已经存储的可执行函数。
如果我们需要执行多次insert语句,但只是每次插入的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时间。如果使用预编译功能,那么只对SQL语句进行一次语法校验和编译,所以效率要高。
1
2
3
4
MySQL执行预编译
MySQL执行预编译分为如三步:

1.执行预编译语句,例如:prepare showUsersByLikeName from 'select * from user where username like ?';
2.设置变量,例如:set @username='%小明%';
3.执行语句,例如:execute showUsersByLikeName using @username;
1
2
3
如果需要再次执行myfun,那么就不再需要第一步,即不需要再编译语句了:

1.设置变量,例如:set @username='%小宋%';
2.执行语句,例如: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管理员界面执行以下操作。
net stop mysql
net start mysql
1
2
使用PreparedStatement执行sql查询
JDBC MySQL驱动5.0.5以后的版本默认PreparedStatement是关闭预编译功能的,所以需要我们手动开启。而之前的JDBC MySQL驱动版本默认是开启预编译功能的。
MySQL数据库服务器的预编译功能在4.1之后才支持预编译功能的。如果数据库服务器不支持预编译功能时,并且使用PreparedStatement开启预编译功能是会抛出异常的。这点非常重要。笔者用的是mysql-connector-jar-5.1.13版本的JDBC驱动。
在我们以前写项目的时候,貌似都没有注意是否开启PreparedStatement的预编译功能,以为它一直都是在使用的,现在看看不开启PreparedStatement的预编译,查看MySQL的日志输出到底是怎么样的。

@Test
public void showUser(){
    //数据库连接
    Connection connection = null;
    //预编译的Statement,使用预编译的Statement提高数据库性能
    PreparedStatement preparedStatement = null;
    //结果 集
    ResultSet resultSet = null;

    try {
        //加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");

        //通过驱动管理类获取数据库链接
        connection =  DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "");
        //定义sql语句 ?表示占位符
        String sql = "select * from user where username = ?";
        //获取预处理statement
        preparedStatement = connection.prepareStatement(sql);

        //设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
        preparedStatement.setString(1, "王五");
        //向数据库发出sql执行查询,查询出结果集
        resultSet =  preparedStatement.executeQuery();

        preparedStatement.setString(1, "张三");
        resultSet =  preparedStatement.executeQuery();
        //遍历查询结果集
        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注入攻击的实现是在PreparedStatement中实现的,和服务器无关。笔者在源码中看到,PreparedStatement对敏感字符已经转义过了。

在PreparedStatement中开启预编译功能
设置MySQL连接URL参数:useServerPrepStmts=true,如下所示。
jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true
这样才能保证mysql驱动会先把SQL语句发送给服务器进行预编译,然后在执行executeQuery()时只是把参数发送给服务器。
再次执行上面的程序看下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
很明显已经进行了预编译,Prepare select * from user where username = ?,这一句就是对sql语句模板进行预编译的日志。好的非常Nice。

注意:

我们设置的是MySQL连接参数,目的是告诉MySQL JDBC的PreparedStatement使用预编译功能(5.0.5之后的JDBC驱动版本需要手动开启,而之前的默认是开启的),不管我们是否使用预编译功能,MySQL Server4.1版本以后都是支持预编译功能的。

cachePrepStmts参数
当使用不同的PreparedStatement对象来执行相同的SQL语句时,还是会出现编译两次的现象,这是因为驱动没有缓存编译后的函数key,导致二次编译。如果希望缓存编译后函数的key,那么就要设置cachePrepStmts参数为true。例如:

jdbc:mysql://localhost:3306/mybatis?useServerPrepStmts=true&cachePrepStmts=true
1
程序代码:

@Test
public void showUser(){
    //数据库连接
    Connection connection = null;
    //预编译的Statement,使用预编译的Statement提高数据库性能
    PreparedStatement preparedStatement = null;
    //结果 集
    ResultSet resultSet = null;

    try {
        //加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");

        //通过驱动管理类获取数据库链接
        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();
        //遍历查询结果集
        while(resultSet.next()){
            System.out.println(resultSet.getString("id")+"  "+resultSet.getString("username"));
        }
        //注意这里必须要关闭当前PreparedStatement对象流,否则下次再次创建PreparedStatement对象的时候还是会再次预编译sql模板,使用PreparedStatement对象后不关闭当前PreparedStatement对象流是不会缓存预编译后的函数key的
        resultSet.close();
        preparedStatement.close();

        preparedStatement=connection.prepareStatement("select * from user where username like ?");
        preparedStatement.setString(1, "%三%");
        resultSet =  preparedStatement.executeQuery();
        //遍历查询结果集
        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对象后都要关闭该PreparedStatement对象流,否则预编译后的函数key是不会缓存的。

Statement执行sql语句是否会对编译后的函数进行缓存
这个不好说,对于每个数据库的具体实现都是不一样的,对于预编译肯定都大体相同,但是对于Statement和普通sql,数据库一般都是先检查sql语句是否正确,然后编译sql语句成为函数,最后执行函数。其实也不乏某些数据库很疯狂,对于普通sql的函数进行缓存。但是目前的主流数据库都不会对sql函数进行缓存的。因为sql语句变化那么多,如果对所有函数缓存,那么对于内存的消耗也是非常巨大的。

如果你不确定普通sql语句的函数是否被存储,那要怎么做呢??

其实还是一个道理,查看MySQL日志记录:检查第二次执行相同sql语句时,是否是直接通过execute来进行查询的。

@Test
public void showUser(){
    //数据库连接
    Connection connection = null;
    //预编译的Statement,使用预编译的Statement提高数据库性能
    PreparedStatement preparedStatement = null;
    //结果 集
    ResultSet resultSet = null;

    try {
        //加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");

        //通过驱动管理类获取数据库链接
        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='小天'");
        //遍历查询结果集
        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='小天'");
        //遍历查询结果集
        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命令,所以并没有存储函数。

总结:
所以到了这里我的疑惑都解开了,PreparedStatement的预编译是数据库进行的,编译后的函数key是缓存在PreparedStatement中的,编译后的函数是缓存在数据库服务器中的。预编译前有检查sql语句语法是否正确的操作。只有数据库服务器支持预编译功能时,JDBC驱动才能够使用数据库的预编译功能,否则会报错。预编译在比较新的JDBC驱动版本中默认是关闭的,需要配置连接参数才能够打开。在已经配置好了数据库连接参数的情况下,Statement对于MySQL数据库是不会对编译后的函数进行缓存的,数据库不会缓存函数,Statement也不会缓存函数的key,所以多次执行相同的一条sql语句的时候,还是会先检查sql语句语法是否正确,然后编译sql语句成函数,最后执行函数。
对于PreparedStatement在设置参数的时候会对参数进行转义处理。
因为PreparedStatement已经对sql模板进行了编译,并且存储了函数,所以PreparedStatement做的就是把参数进行转义后直接传入参数到数据库,然后让函数执行。这就是为什么PreparedStatement能够防止sql注入攻击的原因了。
PreparedStatement的预编译还有注意的问题,在数据库端存储的函数和在PreparedStatement中存储的key值,都是建立在数据库连接的基础上的,如果当前数据库连接断开了,数据库端的函数会清空,建立在连接上的PreparedStatement里面的函数key也会被清空,各个连接之间的预编译都是互相独立的。
使用Statement执行预编译
使用Statement执行预编译就是把上面的原始SQL语句预编译执行一次。

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)其实都没有真正的用上预编译,预编译是要我们自己在参数列表上面配置的,如果我们不手动开启,JDBC驱动程序5.0.5以后版本 默认预编译都是关闭的。

所以我们要在参数列表中配置,例如:

jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true
1
注意:

在MySQL中,既要开启预编译也要开启缓存。因为如果只是开启预编译的话效率还没有不开启预编译效率高,大家可以做一下性能测试,其中性能测试结果在这篇博客中有写到,探究mysql预编译,而在MySQL中开启预编译和开启缓存,其中的查询效率和不开启预编译和不开启缓存的效率是持平的。这里用的测试类是PreparedStatement。

参考资料:

探究mysql预编译

PreparedStatement是如何大幅度提高性能的

参考中文文档下载:MySQL预编译功能

在写这篇文章的时候发生了很多让人恼火的事情,比如网上很多的答案基本上都是错误的,竟然还有人说好??不知道就不要乱说,乱发表博客,误人子弟!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • JDBC基础知识 一、采用JDBC访问数据库的基本步骤: A.载入JDBC驱动程序 B.定义连接URL ...
    java日记阅读 3,832评论 0 20
  • JDBC概述 在Java中,数据库存取技术可分为如下几类:JDBC直接访问数据库、JDO技术、第三方O/R工具,如...
    usopp阅读 3,526评论 3 75
  • 本文主要内容1、JDBC2、DBUtils 01JDBC概念和数据库驱动程序 A: JDBC概念和数据库驱动程序a...
    乘风破浪的姐姐阅读 798评论 0 6
  • 你的破解版密码管理软件还好用不? 适逢七夕🎋,还在赶稿,你问我此时的心情,我只想说工作让我很开心,今天我要加班到凌...
    大海连着天阅读 849评论 0 0
  • 不知是专业原因还是什么,上了大学我就睡得越来越晚了,没有什么理由,却总是到深夜,后来晚上熄灯,就成了只有周五周六不...
    YDWX阅读 480评论 0 1