09_database_05_jdbc-4

第八十章 PreparedStatement接口的基本使用方法

1. 简介

我们在上一章介绍了Statement接口的用法;开发人员通过使用Statement接口能够在数据库中查询、添加、修改和删除数据。为了进一步提高数据操作的性能和安全性,JDBC框架还提供了PreparedStatement接口。一般来说,如果需要反复执行同一条SQL语句的话(只是参数的值不同),PreparedStatement能够提供更好的性能。

PreparedStatement接口继承自Statement接口,因此,PreparedStatement支持Statement接口的大部分功能。与Statement接口不同的是,PreparedStatement具备以下优势。

  1. 性能优势。SQL语句的运行大致可分为两个步骤。首先,第一步是解析和校验SQL语句,检查SQL语句是否符合语法结构。第二步是运行SQL语句。当使用Statement接口时,每执行一条SQL语句,都会运行上述的两个步骤。但是,当使用PreparedStatement接口时,该接口会首先预编译SQL语句;然后再运行SQL语句。这样做的好处是,如果当需要重复执行一条SQL语句时(SQL语句结构相同,但参数值不同),使用PreparedStatement接口只需要解析和编译SQL语句一次。在第二次或者后续的运行中,PreparedStatement不会再检验SQL语句的结构;她会直接运行SQL语句。因此,使用PreparedStatement接口能够获得性能上的提升。值得注意的是,如果执行的SQL语句的结构都不相同的话,使用PreparedStatement接口并不能提升性能,因为每次运行时都需要预编译新的SQL语句。
  2. 使用PreparedStatement更加安全。使用PreparedStatement能够帮助开发人员抵御SQL注入攻击(SQL Injection Attack)。SQL注入攻击是一种通过在用户输入中使用"特殊"参数值来获得额外数据或者破坏系统的一种攻击方式。因为,SQL注入攻击的来源是用户输入的数据,因此,PreparedStatement对注入SQL语句的参数取值有着非常严格的校验。

2. 使用方法

2.1 数据查询

PreparedStatement对象是从java.sql.Connection接口创建出来的。如下面的代码示例所示,开发人员需要使用Connection::prepareStatement()方法创建PreparedStatement对象。在调用该方法时,需要传入一个SQL语句;在该SQL语句中可以直接设置变量的取值,或者使用问号?作为占位符(Place Holder),其值可在后续操作中设置。

在这段代码示例中,我们首先创建了一个字符串对象query,表示查询语句。在查询语句的where子句(where clause)中,我们直接指定查询女学生(gender = 'female'),但是将分数的值留在后续确定。因此,在"grade > ?"的条件中使用了占位符"?"。

当创建出PreparedStatement对象后,这个对象代表的是已经编译了的查询语句(pre-compiled query statement)。我们在示例中执行了两个查询,分别将分数设置在90分和85分,时间设置在2020年。在这个过程中,PreparedStatement是可以重复使用的。在第一次查询时,我们通过调用setInt()方法设置了分数和年份。setInt()方法的第一个参数是查询语句中参数的序号。例如,setInt(1, 90)的意义是将查询语句中第一个参数的值设置为90;即将分数的值设置为90。setInt(2, 2020)的意义是将查询语句中第二个参数的值设置为2020。依次类推。参数的序号从1开始。

当运行第二个查询时,我们只需要修改分数的取值就可以了,因为年份的取值不变,已经在第一次的查询中设置为2020了。所以,在第二次查询中不需要再次设置。executeQuery()方法返回一个ResultSet对象,用于表示查询的结果。我们已在前一章介绍Statement接口时详细讲解了ResultSet对象的使用方法,此处不再累述。

...
// 查询成绩高于某一阈值的所有女学生的姓名
String query = "SELECT name FROM student where gender = 'female' and grade > ? and year = ? ";

try (PreparedStatement stmt = con.prepareStatement(query)) {
    // 查询在2020年成绩高于90分的所有女学生的姓名
    stmt.setInt(1, 90);
    stmt.setInt(2, 2020);
    ResultSet rs1 = stmt.executeQuery();
    ...
    
    // 查询在2020年成绩高于85分的所有女学生的姓名
    stmt.setInt(1, 85);
    ResultSet rs2 = stmt.executeQuery();
    ...
}

另一种常见的使用场景是将查询的参数存放在一个数组或者Map中,通过一个循环来设置其值。

...
// 查询成绩高于某一阈值的所有女学生的姓名
String query = "SELECT name FROM student where gender = 'female' and grade > ? and year = ? ";

Object[] paramsInArray = new Object[2];
paramsInArray[0] = Integer.valueOf(90);
paramsInArray[1] = Integer.valueOf(2020);

try (PreparedStatement stmt = con.prepareStatement(query)) {
    // 查询在2020年成绩高于90分的所有女学生的姓名
    for (int i = 0; i < paramsInArray.length; i++) {
        stmt.setObject(i+1, paramsInArray[i]);
    }
    ResultSet rs = stmt.executeQuery();
    ...
}

Map<Integer, Object> paramsInMap = new HashMap<Integer, Object>();
paramsInMap[1] = Integer.valueOf(90);
paramsInMap[2] = Integer.valueOf(2020);

try (PreparedStatement stmt = con.prepareStatement(query)) {
    // 查询在2020年成绩高于90分的所有女学生的姓名
    // 关键字var在Java 11中引入,会自动检查变量的类型
    for (var entry: paramsInMap.entrySet()) {
        stmt.setObject(entry.key(), entry.value());
    }

    ResultSet rs = stmt.executeQuery();
    ...
}

2.2 数据增、删、更新操作

与数据查询操作类似,PreparedStatement接口也能运行Insert、Delete和Update语句。开发人员也能在语句中使用占位符暂时替代真实的值。PreparedStatement常常用于执行多个Insert语句或者Update语句。

例如,在下面的示例中,我们向数据库新增三名学生信息。

...
String insertStatement = "INSERT INTO student VALUES (?, ?, ?, ?)";

try (PreparedStatement stmt = con.prepareStatement(insertStatement)) {
    /*
     * Students: 
     *   id,    name,    gender,   grade
     * 0001,    Adam,    Male,        90
     * 0002,     Amy,    Female,      85
     * 0003,   David,    Male,        80
     */
    stmt.setInt(1, 0001);
    stmt.setString(2, "Adam");
    stmt.setString(3, "Male");
    stmt.setInt(4, 90);
    stmt.execute();

    stmt.setInt(1, 0002);
    stmt.setString(2, "Amy");
    stmt.setString(3, "Female");
    stmt.setInt(4, 85);
    stmt.execute();

    stmt.setInt(1, 0003);
    stmt.setString(2, "David");
    stmt.setString(3, "Male");
    stmt.setInt(4, 80);
    stmt.execute();
}

在插入新数据之后,可能出于某些原因,我们需要更改一些信息,如下面的例子所示。

...
String updateStatement = "UPDATE student SET grade = ? WHERE id = ?";

try (PreparedStatement stmt = con.prepareStatement(updateStatement)) {
    // 将学号为0001的学生的分数修改为95
    stmt.setInt(1, 95);
    stmt.setInt(2, 0001);
    stmt.execute();

    // 将学号为0002的学生的分数修改为90
    stmt.setInt(1, 90);
    stmt.setInt(2, 0002);
    stmt.execute();

    // 将学号为0003的学生的分数修改为85
    stmt.setInt(1, 85);
    stmt.setInt(2, 0003);
    stmt.execute();
}

如果我们需要删除一些数据的话,也可以使用PreparedStatement接口,如下面的例子所示。

...
String deleteStatement = "DELETE FROM student WHERE id = ?";

try (PreparedStatement stmt = con.prepareStatement(deleteStatement)) {
    // 删除学号为0001的学生的记录
    stmt.setInt(1, 0001);
    stmt.execute();

    // 删除学号为0002的学生的记录
    stmt.setInt(1, 0002);
    stmt.execute();

    // 删除学号为0003的学生的记录
    stmt.setInt(1, 0003);
    stmt.execute();
}

2.3 批处理比较

Statement接口支持使用addBatch()和executeBatch()来批处理执行大量的SQL语句。这种批处理方式支持运行大量的不同的SQL语句。这些SQL语句可以是查询、新增(Insert)、删除(Delete)或者更新(Update)语句。虽然PreparedStatement继承自Statement,但是,PreparedStatement不支持这种批处理

PreparedStatement的性能优势在于执行多个结构相同的SQL语句。这些SQL语句结构和语义相同;它们只是使用的参数的值不同而已。因为在第一次运行这些语句时,PreparedStatement预编译了这些语句的结构,所以,在后续使用中,可以节省SQL语句校验的时间。

3. 小结

本章介绍了PreparedStatement的基本使用方法。在批量运行相同结构的SQL语句时,PreparedStatement能提升SQL运行的性能。另外,PreparedStatement接口能帮助开发人员预防SQL注入攻击。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.