JDBC的Statement 和 PreparedStatement区别

    科技2023-10-08  67

    1. 概念

    Java提供了三种用来执行SQL查询语句的API;Statement、PreparedStatement 和 CallableStatement。其中 Statement 用于通用查询, PreparedStatement 用于执行参数化查询,而 CallableStatement则是用于存储过程。

    Statement继承自Wrapper,一般用于普通的不带参的SQL语句。Statement是不安全的,容易产生SQL注入式攻击。简单理解就是正常的一个SQL语句中添加了一个恶意的SQL语句,比如删除表数据这种。

    PreparedStatement继承自Statement,支持可变参数的SQL,采用预处理编译,大量执行SQL语句可以采用批处理性能比Statement快。同时由于支持可变参数的设置,可以防止SQL注入式攻击,因此相对来更安全。

    CallableStatement继承自PreparedStatement,支持调用存储过程,提供了对输出和输入/输出参数(in/out)的支持;

    2. Statement

    Statement执行代价比较高,每次执行都会编译一次。而PrepareStatement第一次编译时间较长,但是会被预编译和缓冲。当下次再次执行不需要重新编译。所以对于执行次数较少的SQL可以采用Statement来执行,同时Statement不支持可变参数。

    SQL注入式攻击:JDBC提供了一次执行多次SQL语句的"开关"只要打开,对于非正常操作,非常容易造成数据的丢失或者错误。比如在一个简单的查询中加入一句删除表信息的SQL语句。那么这条复合SQL语句必定会造成数据的丢失,这是及其不安全的。

    批量处理:Statement虽然提供了批处理的功能但是很少使用,只能执行静态的SQL语句并且还要防范注入式攻击所以不常使用。

    测试如下,比如当前表中有5条数据,在遇到恶意SQL时导致数据丢失。

    import java.sql.*; import java.util.Scanner; public class JdbcFive { static void print(ResultSet rs) throws SQLException { while(rs.next()){ System.out.print("id = " + rs.getInt("id")); System.out.print(", age = "+rs.getInt("age")); System.out.println(", name = "+rs.getString("name")); System.out.println("----------------------------------------"); } } public static void main(String[] args) { Scanner cin = new Scanner(System.in); try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Connection con = null; Statement st = null; ResultSet rs = null; int rt = 0; // allowMultiQueries = true; 允许同时执行多行语句 // useUnicode = true; 使用Unicode编码 // character = utf8; 使用utf8编码 // useSSL = true; 使用安全连接 String url = "jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&character=utf8&useSSL=true"; String name = "root"; String password = "123456"; String query = "select *from s;"; String sql1 = "insert into s values(6,30,'f');"; String sql2 = "delete from s; insert into s values(7,30,'g')"; String sql = sql1 + sql2; try { con = DriverManager.getConnection(url,name,password); st = con.createStatement(); //建立执行对象 rs = st.executeQuery(query); //查询 print(rs); System.out.println("\n\n\n\n\n"); rt = st.executeUpdate(sql); //恶意SQL语句 rs = st.executeQuery(query); //再次查询 print(rs); //空表 } catch (SQLException throwables) { throwables.printStackTrace(); } finally { try { if(rs != null) rs.close(); if(st != null) st.close(); if(con != null) con.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } }

    3. PreparedStatement

    PreparedStatement的第一次执行消耗是很高的,它的性能体现在后面的重复执行。由于之前有预处理当再次执行这个语句时,JDBC驱动解析当前命令但是不会去重新编译这个语句,当查询次数过多时这是十分省时间的。假设有m次查询,每一次编译的时间复杂度是O(n)。那么Statement的时间复杂度是O(n*m)的。对于预处理的PreparedStatement的时间复杂度可能是O(n)的一次预处理 + m次单次复杂度O(logn)较小的查询时间复杂度O(mlogn),总体时间复杂度是O(n + mlogn),对于m次数过大时这个这是非常一个乐观的复杂度。

    对于Statement无法处理恶意SQL注入进攻是非常可怕的,因此有了更加安全的PreparedStatement用参数匹配来执行,只有当所有的参数都匹配成功了才能执行否则是不执行的。这样就可以执行动态SQL语句同时可以减少或者避免SQL注入攻击,变的更加的安全可靠。测试如下

    import java.sql.*; import java.util.Scanner; public class JdbcFive { static void print(ResultSet rs) throws SQLException { while(rs.next()){ System.out.print("id = " + rs.getInt("id")); System.out.print(", age = "+rs.getInt("age")); System.out.println(", name = "+rs.getString("name")); System.out.println("----------------------------------------"); } } public static void main(String[] args) { Scanner cin = new Scanner(System.in); try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Connection con = null; PreparedStatement ps = null; ResultSet rs = null; int rt = 0; // allowMultiQueries = true; 允许同时执行多行语句 // useUnicode = true; 使用Unicode编码 // character = utf8; 使用utf8编码 // useSSL = true; 使用安全连接 String url = "jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&character=utf8&useSSL=true"; String name = "root"; String password = "123456"; int id = 6; int age = 30; String query = "select *from s;"; String sql1 = "insert into s values("+id+","+age+","; String sql2 = "'f'); delete from s; insert into s values(7,30,'g')"; String sql = sql1 + sql2; String insert = "insert into s (id, age, name) values (?, ?, ?)"; //问号用来参数匹配 try { con = DriverManager.getConnection(url,name,password); ps = con.prepareStatement(insert); rs = ps.executeQuery(query); //查询 print(rs); System.out.println("\n\n\n\n\n"); ps.setInt(1,id); //第一个参数是int类型 ps.setInt(2,age); //第二个参数是int类型 ps.setString(3,sql); //第三个参数是String类型 rt = ps.executeUpdate(); rs = ps.executeQuery(query); print(rs); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { try { if(rs != null) rs.close(); if(ps != null) ps.close(); if(con != null) con.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } }

    执行上面代码之后,由于参数匹配的原因所以恶意的SQL语句被完全当做一个匹配参数来用。

    批量处理:PreparedStatement接口提供了批处理函数可以加快SQL的执行,对于计算机来说IO是最耗时的,addBatch()将一组参数添加到PreparedStatement的对象内部。executeBatch()将一批参数提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组;否则一条也不执行。注意:PreparedStatement中使用批量更新时,要先设置好参数后再使用addBatch()方法加入缓存。批量更新中只能使用更改、删除或插入语句。 import java.sql.*; import java.util.Scanner; public class JdbcFive { static void print(ResultSet rs) throws SQLException { while(rs.next()){ System.out.print("id = " + rs.getInt("id")); System.out.print(", age = "+rs.getInt("age")); System.out.println(", name = "+rs.getString("name")); System.out.println("----------------------------------------"); } } public static void main(String[] args) { Scanner cin = new Scanner(System.in); try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Connection con = null; PreparedStatement ps = null; ResultSet rs = null; int rt = 0; // allowMultiQueries = true; 允许同时执行多行语句 // useUnicode = true; 使用Unicode编码 // character = utf8; 使用utf8编码 // useSSL = true; 使用安全连接 String url = "jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&character=utf8&useSSL=true"; String name = "root"; String password = "123456"; int id = 6; int age = 30; String query = "select *from s;"; String sql = "person"; String insert = "insert into s (id, age, name) values (?, ?, ?)"; //问号用来参数匹配 try { con = DriverManager.getConnection(url,name,password); con.setAutoCommit(false); //关闭自动提交 ps = con.prepareStatement(insert); for(int i = 1;i <= 10;i++){ ps.setInt(1,i); //第一个参数是int类型 ps.setInt(2,i+20); //第二个参数是int类型 ps.setString(3,sql+i); //第三个参数是String类型 ps.addBatch(); //添加到对象内部 } int[] result = ps.executeBatch(); //批量执行成功返回的是一个数组 for(int i = 0;i < result.length;i++){ System.out.print(i+" "); } System.out.println("\n批量提交成功!"); con.commit(); //手动提交 System.out.println("手动提交成功!"); rs = ps.executeQuery(query); print(rs); } catch (SQLException throwables) { try { con.rollback(); } catch (SQLException e) { e.printStackTrace(); } throwables.printStackTrace(); } finally { try { if(rs != null) rs.close(); if(ps != null) ps.close(); if(con != null) con.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } } /* 0 1 2 3 4 5 6 7 8 9 批量提交成功! 手动提交成功! id = 1, age = 21, name = person1 ---------------------------------------- id = 2, age = 22, name = person2 ---------------------------------------- id = 3, age = 23, name = person3 ---------------------------------------- id = 4, age = 24, name = person4 ---------------------------------------- id = 5, age = 25, name = person5 ---------------------------------------- id = 6, age = 26, name = person6 ---------------------------------------- id = 7, age = 27, name = person7 ---------------------------------------- id = 8, age = 28, name = person8 ---------------------------------------- id = 9, age = 29, name = person9 ---------------------------------------- id = 10, age = 30, name = person10 ---------------------------------------- */ 当参数匹配或者从结果集中输出时如果不知道具体的数据类型可以采用setObject 和 getObject的方法进行参数的匹配和获取。因为不是所有的Java字段都和MySQL的字段完全匹配的。 PreparedStatement.setObject(index,value); //字段索引和字段值 ResultSet.getObject(columnLabel); //参数是字段名

    4. 总结

    PreparedStatement可以阻止常见的SQL注入攻击

    PreparedStatement可以执行动态SQL语句

    PreparedStatement采用预编译,大量执行SQL语句时效果比Statement好

    '?'是占位符,采用参数匹配时下标索引是从index = 1开始的不是从0开始,否则会出现异常。

    学习阶段大多数情况不会考虑这些,开发情况建议使PreparedStatement,一切要以安全为重。

    Processed: 0.012, SQL: 8