化妆品销售网站开发与设计,域名查询中国万网,淘宝的网站建设情况,长沙景点排行榜前十名目录 前言一、PreparedStatement是什么二、重点理解预编译三、PreparedStatement基本使用四、Statement和PreparedStatement比较1.PreparedStatement效率高2.PreparedStatement无需拼接参数3.PreparedStatement防止SQL注入 总结 前言 #x1f4dc; 本系列教程适用于JavaWeb初学… 目录 前言一、PreparedStatement是什么二、重点理解预编译三、PreparedStatement基本使用四、Statement和PreparedStatement比较1.PreparedStatement效率高2.PreparedStatement无需拼接参数3.PreparedStatement防止SQL注入 总结 前言 本系列教程适用于JavaWeb初学者、爱好者小白白。我们的天赋并不高可贵在努力坚持不放弃。坚信量最终引发质变厚积薄发。 文中白话居多尽量以小白视角呈现帮助大家快速入门。 我是 蜗牛老师之前网名是 Ongoing蜗牛人如其名干啥都慢所以更新也慢。希望大家多多支持让我动力十足 上篇文章我们学习了 JDBC 编程基本步骤步骤中使用 Statement 执行 SQL 语句。其实还有一个更好的方式就是 PreparedStatement预编译语句。与 Statement 相比有诸多优势目前开发中一般使用 PreparedStatement。所以我们非常有必要进行学习而且日后的持久层框架底层也是使用 PreparedStatement。 一、PreparedStatement是什么
在 JDBC 编程基本步骤中我们创建 Statement语句对象向数据库发送要执行的 SQL 语句。Statement 一般用于实现不带参数的简单 SQL 语句也就是说它执行的是静态 SQL 语句。每次执行 SQL 语句时都会将 SQL 语句编译为数据库可以理解的格式。Statement 的工作原理是将 SQL 语句发送给数据库然后数据库执行该语句并返回结果。
那么 PreparedStatement 是什么呢大家应该也发现了PreparedStatement 比 Statement 多了单词 Prepared理解的重点就是 Prepared它是预编译的意思所以平时被称为预编译的 Statement语句。
PreparedStatement 是一个接口而且它是 Statement 接口的子接口。
// Statement接口
public interface Statement extends Wrapper, AutoCloseable {}// PreparedStatement接口
public interface PreparedStatement extends Statement {}PreparedStatement 接口用于执行动态 SQL 语句。它允许在 SQL 语句中使用占位符? 英文格式问号然后在执行之前将这些占位符?替换为实际的值。PreparedStatement 在执行之前会对 SQL 语句进行预编译这样可以提高执行效率。工作原理是将 SQL 语句发送给数据库之前先将其编译为可执行的格式然后将实际的参数值传递给占位符?。
具体解决什么问题呢我们来看一个需求一个博客网站要根据某个文章编号id查询该文章内容进行展示文章编号100和101分别代表不同的文章。我们来看 SQL 语句
select * from article where id 100;
select * from article where id 101;在上述需求中我们会反复执行一条结构相似的 SQL 语句。其实在日常需求当中经常需要执行 SQL 语句结构基本相似但是执行时的参数值不同。这种 SQL 语句我们可以叫做动态 SQL 语句就可以使用 PreparedStatement 接口SQL 语句中的参数可以使用占位符?代替也就是说占位符?的位置参数未知可以是100可以是101或是其他。带有占位符?SQL 语句如下
select * from article where id ?;需要注意的是 Statement 执行 SQL 语句时是不允许带有占位符?参数的而且 PreparedStatement 执行带有占位符参数的 SQL 语句时参数必须要传入实际的值才可以。
二、重点理解预编译
大概知道 PreparedStatement 是干什么的之后我们来重点理解一下预编译。
预编译是指在执行 SQL 语句之前将 SQL 语句编译为一个预定义的内部格式以便数据库能够更有效地执行。
预编译的过程包括以下几个步骤 语法分析数据库系统会对传入的 SQL 语句进行语法分析检查其是否符合语法规则。 语义分析数据库系统会对 SQL 语句进行语义分析检查表和列是否存在、权限是否足够等。 优化和执行计划生成数据库系统会对 SQL 语句进行优化生成一个最佳的执行计划以便在执行时能够高效地获取数据。
在预编译完成后数据库会将编译后的执行计划存储在缓存中以便下次执行相同的预编译语句时可以直接使用执行计划从而节省了编译的时间和资源。这也是 PreparedStatement 相较于 Statement 的一个优势所在。
当多次执行相同的预编译语句时由于已经完成了编译和优化的步骤预编译的语句可以更快速地执行因为只需传递参数值并执行执行计划而不需要再进行语法分析、语义分析和执行计划生成等步骤。
需要注意的是预编译功能主要适用于需要多次执行相同的 SQL 语句的场景因为预编译的语句在编译时会占用一定的资源。如果只需要执行一次或是每次 SQL 语句都不同那么使用 Statement 可能会更合适。
三、PreparedStatement基本使用
在对 PreparedStatement 有了基本了解后我们进行简单使用。在 JDBC 编程基本步骤中查询了 teacher 表的全部数据在这里将再次使用 teacher 表这次我们往表中添加数据。
在 test-jdbc 项目中新建类 TestPreparedStatement仍然生成 main() 方法。方法中重新编写 JDBC 代码顺带巩固。 JDBC 编程的第一步就是加载驱动第二步是连接 Connection 连接。大家还记得代码如何编写吗
/*** 敲入main根据提示自动生成主函数main()方法* param args*/
public static void main(String[] args) {try {// ①动态加载指定路径下的MySQL JDBC驱动将其注册到DriverManager中。Class.forName(com.mysql.cj.jdbc.Driver);// ②建立到给定数据库URL的连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}
}之前第三步是创建一个 Statement 对象用于向数据库发送 SQL 语句。现在我们改用 PreparedStatement。和 Statement 一样我们需要先拿到 PreparedStatement 对象使用 Connection 中的 prepareStatement(String) 方法获得。我们来看 API
/*** 创建一个PreparedStatement对象用于向数据库发送参数化的SQL语句。* 可以预编译带有或不带有IN参数的SQL语句并将其存储在PreparedStatement对象中。然后可以使用该对象多次有效地执行该语句。 */
PreparedStatement prepareStatement(String sql) throws SQLException;接下来我们来编写第三步使用 Connection 来创建 PreparedStatement 对象。
/*** 敲入main根据提示自动生成主函数main()方法* param args**/
public static void main(String[] args) {try {// ①动态加载指定路径下的MySQL JDBC驱动将其注册到DriverManager中。Class.forName(com.mysql.cj.jdbc.Driver);// ②建立到给定数据库URL的连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// ③使用Connection来创建PreparedStatement对象,将把语句发送到数据库进行预编译。PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}
}上述③代码处使用 Connection 的 prepareStatement(String sql) 方法来创建 PreparedStatement 对象该方法需要传入一个 SQL 语句字符串可以包含占位符。上述 SQL 语句向 teacher 表中插入一条数据由于表中 id 为自增所以插入时不需要给 id 指定值。对于 name、sex、age 三个字段的值使用占位符?进行占位。
PreparedStatement 也提供了 execute()、executeUpdate()、executeQuery() 和 executeLargeUpdate()1.8版本新增方法去执行 SQL 语句。这里我们使用 executeUpdate() 方法去执行 insert 语句。由于执行的 SQL 语句带有占位符参数因此在执行语句前必须为这些参数传入参数值PreparedStatement 提供了一系列的 setXxx(int index, Xxx value) 方法来传入参数值。 我们来看具体代码实现
/*** 敲入main根据提示自动生成主函数main()方法* param args**/
public static void main(String[] args) {try {// ①动态加载指定路径下的MySQL JDBC驱动将其注册到DriverManager中。Class.forName(com.mysql.cj.jdbc.Driver);// ②建立到给定数据库URL的连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// ③使用Connection来创建PreparedStatement对象,将把语句发送到数据库进行预编译。PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));// ④设置参数并执行SQL语句preparedStatement.setString(1, 赵六);preparedStatement.setString(2, 女);preparedStatement.setInt(3, 20);// 执行SQL语句preparedStatement.executeUpdate();} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}
}SQL 语句中的第一个占位符?是表中 name 字段的值为字符串所以这里使用 setString() 方法方法中需要两个参数一个是占位符?的位置从1开始第二个参数是具体的值。其他占位符?操作一样需要注意的是占位符?的位置及该位置传入的参数类型。如果编程时不清楚预编译 SQL 语句中各参数的类型我们可以使用 setObject() 方法传入参数然后由 PreparedStatement 负责类型转换。
最后一步就是关闭资源使用到哪些资源就关闭哪些资源注意当前代码中我们没有使用 Statement 和 ResultSet。
/*** 敲入main根据提示自动生成主函数main()方法* param args**/
public static void main(String[] args) {try {// ①动态加载指定路径下的MySQL JDBC驱动将其注册到DriverManager中。Class.forName(com.mysql.cj.jdbc.Driver);// ②建立到给定数据库URL的连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// ③使用Connection来创建PreparedStatement对象,将把语句发送到数据库进行预编译。PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));// ④设置参数并执行SQL语句preparedStatement.setString(1, 赵六);preparedStatement.setString(2, 女);preparedStatement.setInt(3, 20);// 执行SQL语句preparedStatement.executeUpdate();// ⑤ 关闭资源preparedStatement.close();connection.close();} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}
}我们运行程序控制台无报错通过客户端查看表中记录是否有刚刚插入的赵六数据。 四、Statement和PreparedStatement比较
Statement 和 PreparedStatement 的相同之处在于它们都是用于执行 SQL 语句的接口。它们都可以执行查询和更新操作并且都可以接收参数。不同之处在于 PreparedStatement 可以预编译 SQL 语句并使用占位符?这样可以提高执行效率并且可以防止 SQL 注入攻击。
1.PreparedStatement效率高
我们进行简单测试向 teacher 表分别插入1000条记录进行对比。
import java.sql.*;/*** PreparedStatementVsStatement PreparedStatement和Statement比较** author Ongoing蜗牛* since 2023/8/24 14:25*/
public class PreparedStatementVsStatement {/*** 使用Statement执行SQL语句** param connection 数据库连接对象*/public void insertByStatement(Connection connection){// 以毫秒为单位返回当前时间记录开始时间long start System.currentTimeMillis();try {// 创建Statement对象Statement statement connection.createStatement();// 使用for循环执行插入1000条记录for (int i 0; i 1000; i) {statement.executeUpdate(insert into teacher (name, sex, age) value (张某 i , 男, 20));}} catch (SQLException throwables) {throwables.printStackTrace();}long end System.currentTimeMillis();System.out.println(使用Statement执行耗时 (end - start));}/*** 使用PreparedStatement执行SQL语句** param connection 数据库连接对象*/public void insertByPreparedStatement(Connection connection){// 以毫秒为单位返回当前时间记录开始时间long start System.currentTimeMillis();try {// 创建Statement对象PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));// 使用for循环设置参数值并执行for (int i 0; i 1000; i) {preparedStatement.setString(1, 李某 i);preparedStatement.setString(2, 女);preparedStatement.setInt(3, 23);preparedStatement.executeUpdate();}} catch (SQLException throwables) {throwables.printStackTrace();}long end System.currentTimeMillis();System.out.println(使用PreparedStatement执行耗时 (end - start));}/*** 测试** param args*/public static void main(String[] args) {PreparedStatementVsStatement psvs new PreparedStatementVsStatement();try {// 注册驱动Class.forName(com.mysql.cj.jdbc.Driver);// 建立数据库连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// 调用 使用Statement执行SQL语句 方法psvs.insertByStatement(connection);// 调用 使用PreparedStatement执行SQL语句 方法psvs.insertByPreparedStatement(connection);} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}}
}执行结果如下
使用Statement执行耗时751
使用PreparedStatement执行耗时637通过上述测试代码可知同样是插入1000条记录使用 Statement 需要传入1000条 SQL 语句而使用 PreparedStatement 其实只需要传入一条预编译的 SQL 语句然后对其进行1000次设置参数。从执行多用时间也可以看出 PreparedStatement 的执行效率要高于 Statement。
2.PreparedStatement无需拼接参数
其实 PreparedStatement 还有一个优势是在使用参数时不需要拼接减少 SQL 语句的复杂的。我们看下面的伪代码
// 教师信息
String name 张三;
String sex 男;
int age 20;// 使用Statement执行需要拼接参数
statement.executeUpdate(insert into teacher (name, sex, age) value (name,sex,age));// 使用PreparedStatement执行不需要拼接参数
PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));
preparedStatement.setString(1, name);
preparedStatement.setString(2, sex);
preparedStatement.setInt(3, age);
preparedStatement.executeUpdate();
3.PreparedStatement防止SQL注入 SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句在管理员不知情的情况下实现非法操作以此来实现欺骗数据库服务器执行非授权的任意查询从而进一步得到相应的数据信息。 经典的例子就是登录操作一般正常用户会输入用户和密码进行登录操作系统根据用户输入到数据库表中进行匹配如果找到相应的记录则登录成功否则登录失败。那么我们要通过 SQL 语句的执行去进行匹配操作。这里还是使用 teacher 表简单模拟要求输入的姓名在表中存在就登录成功。正常用户输入姓名系统执行 SQL 匹配。非正常用户会输入特殊字符串匹配。代码如下
import java.sql.*;/*** TestSqlInjection SQL注入** author Ongoing蜗牛* since 2023/8/24 15:22*/
public class TestSqlInjection {/*** 使用姓名登录Statement执行SQL语句* param name 姓名*/public void loginByStatement(String name){// 打印输入的姓名System.out.println(登录姓名 name);// 登录标识boolean flag false;try {// 注册驱动Class.forName(com.mysql.cj.jdbc.Driver);// 建立数据库连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// 创建Statement对象Statement statement connection.createStatement();// 执行SQL语句并返回结果ResultSet resultSet statement.executeQuery(select * from teacher where name name );// 打印执行的SQL语句System.out.println(select * from teacher where name name );// 处理结果while (resultSet.next()){// 返回至少一条记录就登录成功flag true;break;}// 关闭资源resultSet.close();statement.close();connection.close();// 打印输出结果if (flag){System.out.println(登录成功);}else{System.out.println(登录失败);}} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}}/*** 主函数测试* param args*/public static void main(String[] args) {TestSqlInjection testSqlInjection new TestSqlInjection();//String loginName 张三; //数据库中可以匹配登录成功//String loginName 某某; //数据库中匹配不到登录失败String loginName or true or ; //数据库中匹配不到但是登录成功testSqlInjection.loginByStatement(loginName);}
}执行结果如下
登录姓名 or true or
select * from teacher where name or true or
登录成功这回大家明白什么是 SQL 注入了吧。那么使用 PreparedStatement 进行同样的登录操作也会遭遇 SQL 注入登录成功吗
/*** 使用姓名登录PreparedStatement执行SQL语句* param name 姓名*/
public void loginByPreparedStatement(String name){// 打印输入的姓名System.out.println(登录姓名 name);// 登录标识boolean flag false;try {// 注册驱动Class.forName(com.mysql.cj.jdbc.Driver);// 建立数据库连接。Connection connection DriverManager.getConnection(jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezoneAsia/Shanghai, root, root);// 创建PreparedStatement对象预编译SQL语句PreparedStatement preparedStatement connection.prepareStatement(select * from teacher where name ?);// 设置参数preparedStatement.setString(1, name);// 执行SQL语句ResultSet resultSet preparedStatement.executeQuery();// 打印执行的SQL语句System.out.println(preparedStatement.toString());// 处理结果while (resultSet.next()){// 返回至少一条记录就登录成功flag true;break;}// 关闭资源resultSet.close();preparedStatement.close();connection.close();// 打印输出结果if (flag){System.out.println(登录成功);}else{System.out.println(登录失败);}} catch (ClassNotFoundException | SQLException e) {e.printStackTrace();}
}/*** 主函数测试* param args*/
public static void main(String[] args) {TestSqlInjection testSqlInjection new TestSqlInjection();//String loginName 张三; //数据库中可以匹配登录成功//String loginName 某某; //数据库中匹配不到登录失败String loginName or true or ; //数据库中匹配不到但是登录成功//testSqlInjection.loginByStatement(loginName);testSqlInjection.loginByPreparedStatement(loginName);
}我们查看执行结果为登录失败也就是说 PreparedStatement 可以防止 SQL 注入。究其原因很简单PreparedStatement 将 or true or 作为一个参数值传给 name数据库中当然没有该姓名了。
登录姓名 or true or
com.mysql.cj.jdbc.ClientPreparedStatement: select * from teacher where name or true or
登录失败总结
PreparedStatement 接口用于执行动态SQL语句。 它允许在 SQL 语句中使用占位符然后在执行之前将这些占位符替换为实际的值。
预编译是指在执行 SQL 语句之前将 SQL 语句编译为一个预定义的内部格式以便数据库能够更有效地执行。
PreparedStatement 关键步骤
// 使用Connection来创建PreparedStatement对象,将把语句发送到数据库进行预编译。
PreparedStatement preparedStatement connection.prepareStatement(insert into teacher (name, sex, age) value (?, ?, ?));
// 设置参数并执行SQL语句
preparedStatement.setString(1, 赵六);
preparedStatement.setString(2, 女);
preparedStatement.setInt(3, 20);
// 执行SQL语句
preparedStatement.executeUpdate();PreparedStatement 优势
预编译 SQL 语句效率高。无需拼接参数更简单。防止 SQL 注入更安全。
日后 JDBC 编程使用 PreparedStatement 执行 SQL 语句。