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数据库本⾝都⽀持的。但是MySQLServer4.1之前的
版本是不⽀持预编译的。(具体是否包括4.1还得读者们亲⾃试验)
在这⾥,笔者⽤的是MySQL5.6绿⾊版。
MySQL中的预编译功能是这样的
预编译的好处:
MySQL执⾏预编译
MySQL执⾏预编译分为如三步:
如果需要再次执⾏myfun,那么就不再需要第⼀步,即不需要再编译语句了:
如果你看MySQL⽇志记录,你就会看到:
配置MySQL⽇志记录
⼤家平时都使⽤过JDBC中的PreparedStatement接⼝,它有预编译功能。什么是预编译功能呢?它有什么好处呢?
当客户发送⼀条SQL语句给服务器后,服务器总是需要校验SQL语句的语法格式是否正确,然后把SQL语句编译成可执⾏的函数,最后才是执⾏SQL语句。其中
注意:可执⾏函数存储在MySQL服务器中,并且当前连接断开后,MySQL服务器会清除已经存储的可执⾏函数。
如果我们需要执⾏多次inrt语句,但只是每次插⼊的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时间。
1.执⾏预编译语句,例如:prepareshowUrsByLikeNamefrom'lect*fromurwhereurnamelike?';
2.设置变量,例如:t@urname='%⼩明%';
3.执⾏语句,例如:executeshowUrsByLikeNameusing@urname;
1.设置变量,例如:t@urname='%⼩宋%';
2.执⾏语句,例如:executeshowUrsByLikeNameusing@urname;
路径地址可以⾃⼰修改。
配置之后就重启MySQL服务器:
在cmd管理员界⾯执⾏以下操作。
使⽤PreparedStatement执⾏sql查询
JDBCMySQL驱动5.0.5以后的版本默认PreparedStatement是关闭预编译功能的,所以需要我们⼿动开启。⽽之前的JDBC
MySQL驱动版本默认是开启预编译功能的。
MySQL数据库服务器的预编译功能在4.1之后才⽀持预编译功能的。如果数据库服务器不⽀持预编译功能时,并且使⽤
PreparedStatement开启预编译功能是会抛出异常的。这点⾮常重要。笔者⽤的是mysql-connector-jar-5.1.13版本的JDBC驱动。
在我们以前写项⽬的时候,貌似都没有注意是否开启PreparedStatement的预编译功能,以为它⼀直都是在使⽤的,现在看看不开启
PreparedStatement的预编译,查看MySQL的⽇志输出到底是怎么样的。
log-output=FILE
general-log=1
general_log_file="E:"
slow-query-log=1
slow_query_log_file="E:mysql_"
long_query_time=2
netstopmysql
netstartmysql
@Test
publicvoidshowUr(){
//数据库连接
Connectionconnection=null;
//预编译的Statement,使⽤预编译的Statement提⾼数据库性能
PreparedStatementpreparedStatement=null;
//结果集
ResultSetresultSet=null;
try{
//加载数据库驱动
e("");
//通过驱动管理类获取数据库链接
connection=nection("jdbc:mysql://localhost:3306/mybatis","root","");
//定义sql语句?表⽰占位符
Stringsql="lect*fromurwhereurname=?";
//获取预处理statement
preparedStatement=eStatement(sql);
//设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
ing(1,"王五");
这是输出⽇志:
可以看到,在⽇志中并没有看到"prepare"命令来预编译"lect*fromurwhereurname=?"这个sql模板。所以我们⼀般⽤的
PreparedStatement并没有⽤到预编译功能的,只是⽤到了防⽌sql注⼊攻击的功能。防⽌sql注⼊攻击的实现是在PreparedStatement中
实现的,和服务器⽆关。笔者在源码中看到,PreparedStatement对敏感字符已经转义过了。
在PreparedStatement中开启预编译功能
ing(1,"王五");
//向数据库发出sql执⾏查询,查询出结果集
resultSet=eQuery();
ing(1,"张三");
resultSet=eQuery();
//遍历查询结果集
while(()){
n(ing("id")+""+ing("urname"));
}
();
();
n("
#");
}catch(Exceptione){
tackTrace();
}finally{
//释放资源
if(resultSet!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(preparedStatement!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(connection!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
}
}
20Query/*mysql-connector-java-5.1.13(Revision:${on-id})*/SELECT@@_increment_increment
20QuerySHOWCOLLATION
20QuerySETNAMESutf8mb4
20QuerySETcharacter_t_results=NULL
20QuerySETautocommit=1
20Querylect*fromurwhereurname='王五'
20Querylect*fromurwhereurname='张三'
20Quit
设置MySQL连接URL参数:uServerPrepStmts=true,如下所⽰。
jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true
这样才能保证mysql驱动会先把SQL语句发送给服务器进⾏预编译,然后在执⾏executeQuery()时只是把参数发送给服务器。
再次执⾏上⾯的程序看下MySQL⽇志输出:
很明显已经进⾏了预编译,Preparelect*fromurwhereurname=?,这⼀句就是对sql语句模板进⾏预编译的⽇志。好的⾮常Nice。
注意:
我们设置的是MySQL连接参数,⽬的是告诉MySQLJDBC的PreparedStatement使⽤预编译功能(5.0.5之后的JDBC驱动版本需要⼿动
开启,⽽之前的默认是开启的),不管我们是否使⽤预编译功能,MySQLServer4.1版本以后都是⽀持预编译功能的。
cachePrepStmts参数
当使⽤不同的PreparedStatement对象来执⾏相同的SQL语句时,还是会出现编译两次的现象,这是因为驱动没有缓存编译后的函数
key,导致⼆次编译。如果希望缓存编译后函数的key,那么就要设置cachePrepStmts参数为true。例如:
程序代码:
21QuerySHOWWARNINGS
21Query/*mysql-connector-java-5.1.13(Revision:${on-id})*/SELECT@@_increment_increment
21QuerySHOWCOLLATION
21QuerySETNAMESutf8mb4
21QuerySETcharacter_t_results=NULL
21QuerySETautocommit=1
21Preparelect*fromurwhereurname=?
21Executelect*fromurwhereurname='王五'
21Executelect*fromurwhereurname='张三'
21Clostmt
21Quit
jdbc:mysql://localhost:3306/mybatis?uServerPrepStmts=true&cachePrepStmts=true
@Test
publicvoidshowUr(){
//数据库连接
Connectionconnection=null;
//预编译的Statement,使⽤预编译的Statement提⾼数据库性能
PreparedStatementpreparedStatement=null;
//结果集
ResultSetresultSet=null;
try{
//加载数据库驱动
e("");
//通过驱动管理类获取数据库链接
connection=nection("jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true&cachePrepStmts=true","root"
preparedStatement=eStatement("lect*fromurwhereurnamelike?");
ing(1,"%⼩明%");
resultSet=eQuery();
//遍历查询结果集
while(()){
n(ing("id")+""+ing("urname"));
}
//注意这⾥必须要关闭当前PreparedStatement对象流,否则下次再次创建PreparedStatement对象的时候还是会再次预编译sql模板,使⽤PreparedStat
();
();
preparedStatement=eStatement("lect*fromurwhereurnamelike?");
⽇志输出:
注意:每次使⽤PreparedStatement对象后都要关闭该PreparedStatement对象流,否则预编译后的函数key是不会缓存的。
Statement执⾏sql语句是否会对编译后的函数进⾏缓存
这个不好说,对于每个数据库的具体实现都是不⼀样的,对于预编译肯定都⼤体相同,但是对于Statement和普通sql,数据库⼀般都是先
检查sql语句是否正确,然后编译sql语句成为函数,最后执⾏函数。其实也不乏某些数据库很疯狂,对于普通sql的函数进⾏缓存。但是⽬
前的主流数据库都不会对sql函数进⾏缓存的。因为sql语句变化那么多,如果对所有函数缓存,那么对于内存的消耗也是⾮常巨⼤的。
preparedStatement=eStatement("lect*fromurwhereurnamelike?");
ing(1,"%三%");
resultSet=eQuery();
//遍历查询结果集
while(()){
n(ing("id")+""+ing("urname"));
}
();
();
}catch(Exceptione){
tackTrace();
}finally{
//释放资源
if(resultSet!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(preparedStatement!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(connection!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
}
}
24QuerySHOWWARNINGS
24Query/*mysql-connector-java-5.1.13(Revision:${on-id})*/SELECT@@_increment_increment
24QuerySHOWCOLLATION
24QuerySETNAMESutf8mb4
24QuerySETcharacter_t_results=NULL
24QuerySETautocommit=1
24Preparelect*fromurwhereurnamelike?
24Executelect*fromurwhereurnamelike'%⼩明%'
24Executelect*fromurwhereurnamelike'%三%'
24Quit
如果你不确定普通sql语句的函数是否被存储,那要怎么做呢??
其实还是⼀个道理,查看MySQL⽇志记录:检查第⼆次执⾏相同sql语句时,是否是直接通过execute来进⾏查询的。
@Test
publicvoidshowUr(){
//数据库连接
Connectionconnection=null;
//预编译的Statement,使⽤预编译的Statement提⾼数据库性能
PreparedStatementpreparedStatement=null;
//结果集
ResultSetresultSet=null;
try{
//加载数据库驱动
e("");
//通过驱动管理类获取数据库链接
connection=nection("jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true&cachePrepStmts=true","root"
Statementstatement=Statement();
resultSet=eQuery("lect*fromurwhereurname='⼩天'");
//遍历查询结果集
while(()){
n(ing("id")+""+ing("urname"));
}
();
();
statement=Statement();
resultSet=eQuery("lect*fromurwhereurname='⼩天'");
//遍历查询结果集
while(()){
n(ing("id")+""+ing("urname"));
}
();
();
}catch(Exceptione){
tackTrace();
}finally{
//释放资源
if(resultSet!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(preparedStatement!=null){
try{
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
if(connection!=null){
try{
();
⽇志记录:
看⽇志就会知道,都是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语句预编译执⾏⼀次。
();
}catch(SQLExceptione){
//TODOAuto-generatedcatchblock
tackTrace();
}
}
}
}
26QuerySHOWWARNINGS
26Query/*mysql-connector-java-5.1.13(Revision:${on-id})*/SELECT@@_increment_increment
26QuerySHOWCOLLATION
26QuerySETNAMESutf8mb4
26QuerySETcharacter_t_results=NULL
26QuerySETautocommit=1
26Querylect*fromurwhereurname='⼩天'
26Querylect*fromurwhereurname='⼩天'
26Quit
在持久层框架中存在的问题
很多主流持久层框架(MyBatis,Hibernate)其实都没有真正的⽤上预编译,预编译是要我们⾃⼰在参数列表上⾯配置的,如果我们不⼿动
开启,JDBC驱动程序5.0.5以后版本默认预编译都是关闭的。
所以我们要在参数列表中配置,例如:
注意:
在MySQL中,既要开启预编译也要开启缓存。因为如果只是开启预编译的话效率还没有不开启预编译效率⾼,⼤家可以做⼀下性能测试,
其中性能测试结果在这篇博客中有写到,,⽽在MySQL中开启预编译和开启缓存,其中的查询效率和不开启预编译和不开启缓存的效率是
持平的。这⾥⽤的测试类是PreparedStatement。
参考资料:
在写这篇⽂章的时候发⽣了很多让⼈恼⽕的事情,⽐如⽹上很多的答案基本上都是错误的,竟然还有⼈说好??不知道就不要乱说,乱发表
博客,误⼈⼦弟!!
Connectioncon=nection();
Statementstmt=Statement();
eUpdate("preparemyfunfrom'lect*fromt_bookwherebid=?'");
eUpdate("t@str='b1'");
ResultSetrs=eQuery("executemyfunusing@str");
while(()){
(ing(1)+",");
(ing(2)+",");
(ing(3)+",");
n(ing(4));
}
eUpdate("t@str='b2'");
rs=eQuery("executemyfunusing@str");
while(()){
(ing(1)+",");
(ing(2)+",");
(ing(3)+",");
n(ing(4));
}
();
();
();
jdbc:mysql://localhost:3306/mybatis?&uServerPrepStmts=true&cachePrepStmts=true
本文发布于:2022-12-28 12:33:53,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/46650.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |