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语句。其中注意:可执⾏函数存储在MySQL服务器中,并且当前连接断开后,MySQL服务器会清除已经存储的可执⾏函数。
如果我们需要执⾏多次inrt语句,但只是每次插⼊的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时间。MySQL执⾏预编译
MySQL执⾏预编译分为如三步:
1.执⾏预编译语句,例如:prepare showUrsByLikeName from 'lect * from ur where urname like ?';
2.设置变量,例如:t @urname='%⼩明%';
3.执⾏语句,例如:execute showUrsByLikeName using @urname;
如果需要再次执⾏myfun,那么就不再需要第⼀步,即不需要再编译语句了:
1.设置变量,例如:t @urname='%⼩宋%';
2.执⾏语句,例如:execute showUrsByLikeName using @urname;
如果你看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
配置之后就重启MySQL服务器:
在cmd管理员界⾯执⾏以下操作。
net stop mysql
net start mysql
使⽤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 showUr(){
//数据库连接
Connection connection = null;
//预编译的Statement,使⽤预编译的Statement提⾼数据库性能
PreparedStatement preparedStatement = null;
//结果集
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("sql.jdbc.Driver");
//通过驱动管理类获取数据库链接
connection = Connection("jdbc:mysql://localhost:3306/mybatis", "root", "");
//定义sql语句 ?表⽰占位符
String sql = "lect * from ur where urname = ?";
//获取预处理statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
preparedStatement.tString(1, "王五");
preparedStatement.tString(1, "王五");
//向数据库发出sql执⾏查询,查询出结果集
resultSet = uteQuery();
preparedStatement.tString(1, "张三");
resultSet = uteQuery();
//遍历查询结果集
()){
System.out.String("id")+" "+String("urname"));
}
resultSet.clo();
preparedStatement.clo();
System.out.println("#############################");
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
if(resultSet!=null){
try {
resultSet.clo();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.clo();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.clo();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
这是输出⽇志:
20 Query /* mysql-connector-java-5.1.13 ( Revision: ${vision-id} ) */SELECT @@ssion.auto_increment_increment
20 Query SHOW COLLATION
20 Query SET NAMES utf8mb4
20 Query SET character_t_results = NULL
20 Query SET autocommit=1
20 Query lect * from ur where urname = '王五'
20 Query lect * from ur where urname = '张三'
20 Quit
可以看到,在⽇志中并没有看到"prepare"命令来预编译"lect * from ur where urname = ?"这个sql模板。所以我们⼀般⽤的PreparedStatement并没有⽤到预编译功能的,只是⽤到了防⽌sql注⼊攻击的功能。防⽌sql注⼊攻击的实现是在PreparedStatement中实现的,和服务器⽆关。笔者在源码中看到,PreparedStatement对敏感字符已经转义过了。
在PreparedStatement中开启预编译功能
设置MySQL连接URL参数:uServerPrepStmts=true,如下所⽰。
jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true
这样才能保证mysql驱动会先把SQL语句发送给服务器进⾏预编译,然后在执⾏executeQuery()时只是把参数发送给服务器。
再次执⾏上⾯的程序看下MySQL⽇志输出:
21 Query SHOW WARNINGS
21 Query /* mysql-connector-java-5.1.13 ( Revision: ${vision-id} ) */SELECT @@ssion.auto_increment_increment
21 Query SHOW COLLATION
21 Query SET NAMES utf8mb4
21 Query SET character_t_results = NULL
21 Query SET autocommit=1
21Prepare lect * from ur where urname = ?
21Execute lect * from ur where urname = '王五'
21Execute lect * from ur where urname = '张三'
21Clo stmt
21 Quit
很明显已经进⾏了预编译,Prepare lect * from ur where urname = ?,这⼀句就是对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?uServerPrepStmts=true&cachePrepStmts=true
程序代码:
@Test
public void showUr(){
//数据库连接
Connection connection = null;
//预编译的Statement,使⽤预编译的Statement提⾼数据库性能
PreparedStatement preparedStatement = null;
//结果集
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("sql.jdbc.Driver");
//通过驱动管理类获取数据库链接
connection = Connection("jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true&cachePrepStmts=true", "root"
preparedStatement=connection.prepareStatement("lect * from ur where urname like ?");
preparedStatement.tString(1, "%⼩明%");
resultSet = uteQuery();
//遍历查询结果集
()){
System.out.String("id")+" "+String("urname"));
}
//注意这⾥必须要关闭当前PreparedStatement对象流,否则下次再次创建PreparedStatement对象的时候还是会再次预编译sql模板,使⽤PreparedStat
resultSet.clo();
preparedStatement.clo();
preparedStatement=connection.prepareStatement("lect * from ur where urname like ?");
preparedStatement=connection.prepareStatement("lect * from ur where urname like ?");
preparedStatement.tString(1, "%三%");
resultSet = uteQuery();
//遍历查询结果集
()){
System.out.String("id")+" "+String("urname"));
}
resultSet.clo();
preparedStatement.clo();
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
if(resultSet!=null){
try {
resultSet.clo();
} catch (SQLException e) {
/
/ TODO Auto-generated catch block
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.clo();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.clo();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
⽇志输出:
24 Query SHOW WARNINGS
24 Query /* mysql-connector-java-5.1.13 ( Revision: ${vision-id} ) */SELECT @@ssion.auto_increment_increment
24 Query SHOW COLLATION
24 Query SET NAMES utf8mb4
24 Query SET character_t_results = NULL
24 Query SET autocommit=1
24Prepare lect * from ur where urname like ?
24Execute lect * from ur where urname like'%⼩明%'
24Execute lect * from ur where urname like'%三%'
24 Quit
注意:每次使⽤PreparedStatement对象后都要关闭该PreparedStatement对象流,否则预编译后的函数key是不会缓存的。Statement执⾏sql语句是否会对编译后的函数进⾏缓存
这个不好说,对于每个数据库的具体实现都是不⼀样的,对于预编译肯定都⼤体相同,但是对于Statement和普通sql,数据库⼀般都是先检查sql语句是否正确,然后编译sql语句成为函数,最后执⾏函数。其实也不乏某些数据库很疯狂,对于普通sql的函数进⾏缓存。但是⽬前的主流数据库都不会对sql函数进⾏缓存的。因为sql语句变化那么多,如果对所有函数缓存,那么对于内存的消耗也是⾮常巨⼤的。