[疯狂Java]JDBC:PreparedStatement预编译执行SQL语句

时间:2022-12-27 11:55:56

1. SQL语句的执行过程——Statement直接执行的弊病:

    1) SQL语句和编程语言一样,仅仅就会普通的文本字符串,首先数据库引擎无法识别这种文本字符串,而底层的CPU更不理解这些文本字符串(只懂二进制机器指令),因此SQL语句在执行之前肯定需要编译的;

    2) SQL语句的执行过程:提交SQL语句 -> 数据库引擎对SQL语句进行编译得到数据库可执行的代码 -> 执行SQL代码;

    3) 现在再来看Statement的执行机制:

         i. Statement的execute系列方法直接将SQL语句作为参数传入并提交给数据库执行;

         ii. 也就是说每提交一次都需要先经过编译然后再执行;

         iii. 那么有一个最大的问题就是如果一条SQL语句需要再短时间内被反复执行,那么每次都需要经过编译这样不是效率非常非常低吗??

!!可能你会问哪有需要反复大量执行的相同语句呢?仔细一想可能是的,因此上面说的并不完全精确,精确地讲应该是反复执行一系列模型相似的语句,比如:

[sql]  view plain  copy   [疯狂Java]JDBC:PreparedStatement预编译执行SQL语句 [疯狂Java]JDBC:PreparedStatement预编译执行SQL语句
  1. insert into table1 values(1, "Peter");  
!你每次执行时只是values中的值不同,但是总体的语句还是insert into语句,那么你每次提交都需要编译岂不是会把大把时间浪费在编译上面了,非常不值;


2. PreparedStatement的预编译机制——类似于Properties配置文件:

    1) 通过Connection(conn)还可以得到另一种SQL语句对象,即PreparedStatement,该方法就是:PreparedStatement Connection.prepareStatement(String sql);

    2) 注意细节:这里就不是create了,而是准备一个SQL语句句柄,精确地讲是一个PreparedStatement语句句柄,并且创建该句柄时直接传入了SQL语句;

    3) 预编译机制:

         i. 调用prepareStatement时会直接将该SQL语句提交给数据库进行编译,得到的PreparedStatement句柄其实是一个预编译好的SQL语句;

         ii. 之后调用PreparedStatement的execute方法(其execute系列方法都是无参的),就直接将该预编译的语句提交给数据库直接运行而不需要再编译一次了;

         iii. 因此这种方法只需要编译一次就够了,后面就是直接提交执行无需再编译,因此效率最高;

    4) 而预编译语句最大的特点就是支持占位符(支持的占位符就是?,代表任意长度的字符串),比如:insert into table1 values(null, ?, ?);

!!也就是说可以用带占位符的SQL语句来创建预编译SQL句柄:PreparedStatement pstmt = conn.prepareStatement("insert into table1 values(null, ?, ?)");

!!这样的语句也能通过,也可以成功编译,并且可以再后期决定这些占位符具体的值,即使改变这些值后依然不需要编译而直接提交运行;

    5) 设定占位符具体的值:

        i. 可以使用PreparedStatement的setXxx方法设定预编译语句中占位符的值;

        ii. 其原型是这个模式的:void PreparedStatement setXxx(int parameterIndex, Xxx x);

        iii. Xxx几乎涵盖了所有Java基础类型(String、int、double、Date等等);

        iv. parameterIndex代表语句中第几个占位符(从1开始),而x就是具体设定的值;

    6) 设置好占位符的值之后无需编译可以直接提交执行;

!!这种机制其实是跟Properties配置文件完全一样,修改值后无需编译即可运行!

    7) 直接提交执行:

         i. 使用PreparedStatement的execute系列方法即可,和Statement的execute系列方法相对应,只不过无需SQL语句参数了,因为已经存在预编译的SQL语句了,因此都是无参的,就表示直接提交执行;

         ii. 方法:

             a. ResultSet PreparedStatement.executeQuery();

             b. int PreparedStatement.executeUpdate();

             c. boolean PreparedStatement.execute();

!!返回值的意义和Statement的完全相同;

3. 比较直接提交和预编译运行的执行效率(各执行100次):

[java]  view plain  copy   [疯狂Java]JDBC:PreparedStatement预编译执行SQL语句 [疯狂Java]JDBC:PreparedStatement预编译执行SQL语句
  1. public class Test {  
  2.   
  3.     private String driver;  
  4.     private String url;  
  5.     private String user;  
  6.     private String pass;  
  7.       
  8.     private void insertUseStatement() throws Exception {  
  9.         long start = System.currentTimeMillis();  
  10.         try ( // 过了try块会直接释放连接资源  
  11.             Connection conn = DriverManager.getConnection(url, user, pass);  
  12.             Statement stmt = conn.createStatement()  
  13.         ) {  
  14.             for (int i = 0; i < 100; i++) {  
  15.                 stmt.executeUpdate("insert into student_table values(" + "null, '姓名" + i + "', 1)");  
  16.             }  
  17.             System.out.println("使用Statment耗时:" + (System.currentTimeMillis() - start));  
  18.         }  
  19.     }  
  20.       
  21.     private void insertUsePreparedStatement() throws Exception {  
  22.         long start = System.currentTimeMillis();  
  23.         try ( // 因此需要在另一个方法中重新连接  
  24.             Connection conn = DriverManager.getConnection(url, user, pass);  
  25.             PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null, ?, 1)")  
  26.         ) {  
  27.             for (int i = 0; i < 100; i++) {  
  28.                 pstmt.setString(1,  "姓名" + i);  
  29.                 pstmt.executeUpdate();  
  30.             }  
  31.             System.out.println("使用PreparedStatement耗时:" + (System.currentTimeMillis() - start));  
  32.         }  
  33.           
  34.     }  
  35.     public void init() throws Exception {  
  36.         Properties props = new Properties();  
  37.         props.load(new FileInputStream("mysql.ini"));  
  38.         driver = props.getProperty("driver");  
  39.         url = props.getProperty("url");  
  40.         user = props.getProperty("user");  
  41.         pass = props.getProperty("pass");  
  42.           
  43.         insertUseStatement();  
  44.         insertUsePreparedStatement();         
  45.     }  
  46.   
  47.     public static void main(String[] args) throws Exception {  
  48.         new Test().init();  
  49.     }  
  50.   
  51. }  
!!可以看到预编译比直接提交少用很多时间;


4. 预编译SQL的安全性能:

    1) 首先最明显的一点就是Statement不支持占位符,因此SQL语句中包含可变内容时必须要进行字符串拼接,而字符串拼接不仅加大了编程的难度,降低了代码的可读性,而且非常容易发生因拼接错误而导致地极难发现的bug,因此从这点来看PreparedStatement更加安全;

    2) 其次是字符串拼接容易埋下SQL注入的漏洞:

         i. SQL注入是指黑客在应用程序端恶意地往查询信息中填写SQL语句实现入侵(因为客户端输入的要查询的信息往往都是一些正常信息,例如姓名、电话、学号等,没人会无聊地往里面输入代码之类的东西);

         ii. 一个典型的例子:比如SQL语句的目的是select * from member_table where name = input_name and pass = input_pass; input_name和input_pass是用户在客户端输入框中输入的账号名和登陆密码,如果该查询语句能查询到该用户(即返回记录不为空)就表示该用户登陆成功;

如果用预编译占位符来表示该语句就是:select * from member_table where name = ? and pass = ?;   // 然后后期用input_name和input_pass来填补占位符,这没什么问题

但如果用Statement拼接的方式来写该语句就是:"select * from member_table where name = '" + input_name + "' and pass = '" + input_pass +"'";,而此时如果黑客在任意一个输入框(账户名或者密码)中填入'or true or'(就比如账户名输入框吧),那么得到的结果就是:

select * from member_table where name = '' or true or '' and pass = '';

!!也就是说最后的逻辑表达式变成了name = ''、true、'' and pass = ''三者通过or连接在了一起,因为or了一个true因此整个where表达式的结果都是true,因此必然会select处记录,因此即使这样也可以正常登陆!!这就被成功入侵了

         iii. 这最主要是由于不带占位符的拼接必须要用单引号'来包裹SQL字符串,而占位符的填写无需单引号,JDBC会自动将Java变量转换成纯字符串然后再自动加上SQL单引号填入占位符中,即使填入的变量是String str = "'Lala'",那么JDBC也会将其中的单引号' '转化成纯字符单引号处理,而不会被当做SQL的特殊字符单引号'来处理,因为在SQL中单引号'是字符串常量符号!


5. 占位符使用问题注意:

    1) 占位符只能占位SQL语句中的普通值,决不能占位表名、列名、SQL关键字(select、insert等);

    2) 原因很简单,以为PreparedStatement的SQL语句是要预编译的,如果关键字、列名、表名等被占位那就直接代表该SQL语句语法错误而无法编译,会直接抛出异常,因此只有不影响编译的部分可用占位符占位!!