前言

最近听一个老师讲了公开课,在其中讲到了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的日志输出到底是怎么样的。

    @Testpublic 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 = ?";//获取预处理statementpreparedStatement = 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 blocke.printStackTrace();}}if(preparedStatement!=null){try {preparedStatement.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}if(connection!=null){try {connection.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.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_increment20 Query SHOW COLLATION20 Query SET NAMES utf8mb420 Query SET character_set_results = NULL20 Query SET autocommit=120 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 WARNINGS21 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment21 Query SHOW COLLATION21 Query SET NAMES utf8mb421 Query SET character_set_results = NULL21 Query SET autocommit=121 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

程序代码:

    @Testpublic 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 blocke.printStackTrace();}}if(preparedStatement!=null){try {preparedStatement.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}if(connection!=null){try {connection.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.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 WARNINGS24 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment24 Query SHOW COLLATION24 Query SET NAMES utf8mb424 Query SET character_set_results = NULL24 Query SET autocommit=124 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来进行查询的。

    @Testpublic 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 blocke.printStackTrace();}}if(preparedStatement!=null){try {preparedStatement.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}if(connection!=null){try {connection.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.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 WARNINGS26 Query /* mysql-connector-java-5.1.13 ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment26 Query SHOW COLLATION26 Query SET NAMES utf8mb426 Query SET character_set_results = NULL26 Query SET autocommit=126 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预编译功能

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

JDBC:深入理解PreparedStatement和Statement相关推荐

  1. JDBC 笔记4 PreparedStatement 与Statement 的区别

    1  有安全性 PreparedStatement 可以由于不是使用拼接,防止了sql注入,提高了安全性. 2  更方便 PreparedStatement 可以自动对类型进行转换,代码可读性,可维护 ...

  2. JDBC:PreparedStatement与Statement在使用时的区别

    PreparedStatement与Statement在使用时的区别 1.Statement: a.写sql语句 b.然后再执行executeUpdate(sql)或executeQuery(sql) ...

  3. javaweb实训第四天下午——JDBC深入理解

    JDBC深入理解 1.课程介绍 2. PreparedStatement查询 2.1 回顾jdbc 2.2 PreparedStatement的引入 2.2.1 PreparedStatement介绍 ...

  4. preparedStatement和Statement区别

    原文:https://blog.csdn.net/xuebing1995/article/details/72235380 一.概念 PreparedStatement是用来执行SQL查询语句的API ...

  5. Java中PreparedStatement和Statement的用法区别

    Java中PreparedStatement和Statement的用法区别 (2012-08-01 11:06:44) 转载▼ 标签: 杂谈   1. PreparedStatement接口继承Sta ...

  6. 应该始终以PreparedStatement代替Statement

    在JDBC应用中,如果你已经是稍有水平开发者,你就应该始终以PreparedStatement代替Statement.也就是说,在任何时候都不要使用Statement 一.代码的可读性和可维护性. 虽 ...

  7. java中jdbc的封装笔记_JDBC封装学习笔记(三)---面向对象的JDBC,使用preparedStatement...

    使用PreparedStatement对象:为什么要使用PreparedStatement 原因:(1)使用Statement需要拼接SQL,太费劲,也容易出错. String sql = " ...

  8. java PreparedStatement和statement的区别

    1. PreparedStatement接口继承Statement, PreparedStatement 实例包含已编译的 SQL 语句,所以其执行速度要快于 Statement 对象.2.作为 St ...

  9. preparedStatement和Statement 有什么不一样

    1. PreparedStatement接口继承Statement, PreparedStatement 实例包含已编译的 SQL 语句,所以其执行速度要快于 Statement 对象. 2.作为 S ...

  10. 数据库中关于preparedStatement和Statement分析

    引用: http://zhidao.baidu.com/link?url=GT7fotu8j4V0Yf_gYwcceE1u2MsgT-6NKz18K_neo715rE-V11Ny-EHW4OZNkhF ...

最新文章

  1. Tornado推出2.0版
  2. redhat6.5 配置使用centos的yum源
  3. 高并发或高负载下的系统设计
  4. 位置高度ios 开发中跟绘图相关的CGFloat,CGPoint,CGSize,CGRect,CGRectZero
  5. AJPFX学习笔记JavaAPI之String类
  6. 影响中国发展的七大垂直搜索引擎
  7. 优化mysql slave的同步速度
  8. 基于 registry 搭建 Docker 私有镜像仓库
  9. C++ 通讯录设计(七)
  10. Java程序员晋升之路:“Java高级核心知识全面解析
  11. inputstream是否一定要close_新车是否需要底盘保养?一定要喷底盘防锈漆吗?
  12. 计算机的超级终端程序无法使用,超级终端,教您win7怎么添加超级终端
  13. 线性表的链式存储结构
  14. 基于GeoEvent Processor的物联网应用案例赏析
  15. MATLAB 基本操作
  16. mos管 rl_三极管与MOS管工作状态图解分享
  17. 苹果电脑为什么要换 CPU:Intel 与 ARM 的战争
  18. JS,统计图表大全--三、饼形图(饼图及环形图)
  19. 谈谈四天三夜的感受【记录篇】
  20. i7 10875h和i7 9750h对比差距大吗

热门文章

  1. 【BZOJ】【2730】【HNOI2012】矿场搭建
  2. ASP.NET Ajax 实现无刷新分页
  3. 字节码指令之类型转换指令
  4. SpringBoot整合RabbitMQ之Fanout Exchange扇形(广播式)交换机(学习总结)
  5. FunctionalInterface注解
  6. 生产环境一次诡异的空指针问题,反转了4次
  7. JQuery控制div外点击隐藏,div内点击不会隐藏
  8. java基础-基础类型包装类型
  9. IT十八掌掌第十一天课程总结
  10. 配置F5 负载均衡(转)