【JavaEE基础学习打卡06】JDBC之进阶学习PreparedStatement

目录


前言

📜 本系列教程适用于JavaWeb初学者、爱好者,小白白。我们的天赋并不高,可贵在努力,坚持不放弃。坚信量最终引发质变,厚积薄发。

🚀 文中白话居多,尽量以小白视角呈现,帮助大家快速入门。

🎅 我是 蜗牛老师 ,之前网名是 Ongoing蜗牛,人如其名,干啥都慢,所以更新也慢。希望大家多多支持,让我动力十足!

上篇文章我们学习了 JDBC 编程基本步骤,步骤中使用 Statement 执行 SQL 语句。其实还有一个更好的方式,就是 PreparedStatement,预编译语句。与 Statement 相比有诸多优势,目前开发中一般使用 PreparedStatement。所以我们非常有必要进行学习,而且日后的持久层框架底层也是使用 PreparedStatement


一、PreparedStatement是什么

在 JDBC 编程基本步骤中,我们创建 Statement(语句)对象,向数据库发送要执行的 SQL 语句。Statement 一般用于实现不带参数的简单 SQL 语句,也就是说它执行的是静态 SQL 语句。每次执行 SQL 语句时,都会将 SQL 语句编译为数据库可以理解的格式。Statement 的工作原理是将 SQL 语句发送给数据库,然后数据库执行该语句并返回结果。

那么 PreparedStatement 是什么呢?大家应该也发现了,PreparedStatementStatement 多了单词 Prepared,理解的重点就是 Prepared,它是预编译的意思,所以平时被称为预编译的 Statement(语句)。

PreparedStatement 是一个接口,而且它是 Statement 接口的子接口。

java 复制代码
// Statement接口
public interface Statement extends Wrapper, AutoCloseable {}

// PreparedStatement接口
public interface PreparedStatement extends Statement {}

PreparedStatement 接口用于执行动态 SQL 语句。它允许在 SQL 语句中使用占位符(? 英文格式问号),然后在执行之前将这些占位符(?)替换为实际的值。PreparedStatement 在执行之前会对 SQL 语句进行预编译,这样可以提高执行效率。工作原理是将 SQL 语句发送给数据库之前,先将其编译为可执行的格式,然后将实际的参数值传递给占位符(?)。

具体解决什么问题呢?我们来看一个需求:一个博客网站要根据某个文章编号(id)查询该文章内容进行展示,文章编号100和101分别代表不同的文章。我们来看 SQL 语句:

sql 复制代码
select * from article where id = 100;
select * from article where id = 101;

在上述需求中,我们会反复执行一条结构相似的 SQL 语句。其实在日常需求当中,经常需要执行 SQL 语句结构基本相似,但是执行时的参数值不同。这种 SQL 语句我们可以叫做动态 SQL 语句,就可以使用 PreparedStatement 接口,SQL 语句中的参数可以使用占位符(?)代替,也就是说占位符(?)的位置参数未知,可以是100,可以是101或是其他。带有占位符(?)SQL 语句如下:

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 连接。大家还记得代码如何编写吗?

java 复制代码
/**
 * 敲入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?serverTimezone=Asia/Shanghai", "root", "root");
   
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
}

之前第三步是创建一个 Statement 对象,用于向数据库发送 SQL 语句。现在我们改用 PreparedStatement。和 Statement 一样我们需要先拿到 PreparedStatement 对象,使用 Connection 中的 prepareStatement(String) 方法获得。我们来看 API:

java 复制代码
/**
 * 创建一个PreparedStatement对象,用于向数据库发送参数化的SQL语句。
 * 可以预编译带有或不带有IN参数的SQL语句,并将其存储在PreparedStatement对象中。然后可以使用该对象多次有效地执行该语句。 
 */
PreparedStatement prepareStatement(String sql) throws SQLException;

接下来我们来编写第三步:使用 Connection 来创建 PreparedStatement 对象。

java 复制代码
/**
 * 敲入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?serverTimezone=Asia/Shanghai", "root", "root");
        // ③使用Connection来创建PreparedStatement对象,将把语句发送到数据库进行预编译。
        PreparedStatement preparedStatement = connection.prepareStatement("insert into teacher (name, sex, age) value (?, ?, ?)");

    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
}

上述③代码处,使用 ConnectionprepareStatement(String sql) 方法来创建 PreparedStatement 对象,该方法需要传入一个 SQL 语句字符串,可以包含占位符。上述 SQL 语句向 teacher 表中插入一条数据,由于表中 id 为自增,所以插入时不需要给 id 指定值。对于 namesexage 三个字段的值使用占位符(?)进行占位。

PreparedStatement 也提供了 execute()executeUpdate()executeQuery()executeLargeUpdate()(1.8版本新增)方法去执行 SQL 语句。这里我们使用 executeUpdate() 方法去执行 insert 语句。由于执行的 SQL 语句带有占位符参数,因此在执行语句前必须为这些参数传入参数值,PreparedStatement 提供了一系列的 setXxx(int index, Xxx value) 方法来传入参数值。

我们来看具体代码实现:

java 复制代码
/**
 * 敲入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?serverTimezone=Asia/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 负责类型转换。

最后一步就是关闭资源,使用到哪些资源就关闭哪些资源,注意当前代码中我们没有使用 StatementResultSet

java 复制代码
/**
 * 敲入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?serverTimezone=Asia/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比较

StatementPreparedStatement 的相同之处在于它们都是用于执行 SQL 语句的接口。它们都可以执行查询和更新操作,并且都可以接收参数。不同之处在于 PreparedStatement 可以预编译 SQL 语句并使用占位符(?),这样可以提高执行效率,并且可以防止 SQL 注入攻击。

1.PreparedStatement效率高

我们进行简单测试,向 teacher 表分别插入1000条记录进行对比。

java 复制代码
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?serverTimezone=Asia/Shanghai", "root", "root");

            // 调用 使用Statement执行SQL语句 方法
            psvs.insertByStatement(connection);
            // 调用 使用PreparedStatement执行SQL语句 方法
            psvs.insertByPreparedStatement(connection);

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }
}

执行结果如下:

bash 复制代码
使用Statement执行耗时:751
使用PreparedStatement执行耗时:637

通过上述测试代码可知,同样是插入1000条记录,使用 Statement 需要传入1000条 SQL 语句,而使用 PreparedStatement 其实只需要传入一条预编译的 SQL 语句,然后对其进行1000次设置参数。从执行多用时间也可以看出 PreparedStatement 的执行效率要高于 Statement

2.PreparedStatement无需拼接参数

其实 PreparedStatement 还有一个优势是在使用参数时,不需要拼接,减少 SQL 语句的复杂的。我们看下面的伪代码:

java 复制代码
// 教师信息
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 匹配。非正常用户会输入特殊字符串匹配。代码如下:

java 复制代码
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?serverTimezone=Asia/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);
    }
}

执行结果如下:

bash 复制代码
登录姓名:' or true or '
select * from teacher where name = '' or true or ''
登录成功!

这回大家明白什么是 SQL 注入了吧。那么使用 PreparedStatement 进行同样的登录操作,也会遭遇 SQL 注入登录成功吗?

java 复制代码
/**
 * 使用姓名登录,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?serverTimezone=Asia/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,数据库中当然没有该姓名了。

bash 复制代码
登录姓名:' or true or '
com.mysql.cj.jdbc.ClientPreparedStatement: select * from teacher where name = '' or true or ''
登录失败!

总结

PreparedStatement 接口用于执行动态SQL语句。 它允许在 SQL 语句中使用占位符,然后在执行之前将这些占位符替换为实际的值。

预编译是指在执行 SQL 语句之前,将 SQL 语句编译为一个预定义的内部格式,以便数据库能够更有效地执行。

PreparedStatement 关键步骤:

java 复制代码
// 使用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 语句。

相关推荐
SuperherRo4 天前
Web开发-JavaEE应用&ORM框架&SQL预编译&JDBC&MyBatis&Hibernate&Maven
前端·sql·java-ee·maven·mybatis·jdbc·hibernate
希忘auto5 天前
理解Java数据库编程之JDBC
java·jdbc·idea
考虑考虑21 天前
JDK21中的Switch模式匹配
java·后端·java ee
考虑考虑23 天前
JDK14中的switch
java·后端·java ee
奔跑吧邓邓子24 天前
【商城实战(23)】筑牢安全防线,防范常见漏洞
安全·springboot·uniapp·csrf·element plus·sql注入·商城实战
狂奔小菜鸡25 天前
垃圾收集器收集的垃圾是什么?
java·jvm·java ee
考虑考虑1 个月前
JDK23中的instanceof
java·后端·java ee
考虑考虑1 个月前
JDK23支持switch传入long类型
java·后端·java ee
考虑考虑1 个月前
Jdk17中的Stream.toList()
java·后端·java ee
考虑考虑1 个月前
MyCat2使用
java·后端·java ee