【JDBC系列】- 核心API之preparedstatement用法

核心API之preparedstatement用法

生命不息,写作不止
继续踏上学习之路,学之分享笔记
总有一天我也能像各位大佬一样
博客首页   @怒放吧德德  To记录领地
分享学习心得,欢迎指正,大家一起学习成长!

【JDBC系列】- 核心API之preparedstatement用法_第1张图片

引言

上一篇jdbc系列文章中介绍了概念与statement的使用,但是statement会有一些缺处,所以这篇来学习一下preparedstatement这个API的用法。

Statement的缺点

Statement的最大缺点是会有SQL注入的风险。使用Statement时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。

例如以下SQL语句

String username = "lyd";
String sql = "SELECT * FROM tb_user WHERE username = '" + username + "'";

假设username是通过输入传过来的,如果再没做校验的情况下,当输入的值为lyd' or '1' = '1最后形成的SQL语句就变成了

String sql = "SELECT * FROM tb_user WHERE username = 'lyd' or '1' = '1'";

这样我们就能查到指定数据之外的数据了,这就会造成安全隐患。

更多缺点如下

  1. SQL注入风险: 使用Statement时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。
  2. 性能问题: Statement执行SQL查询时,每次都要将SQL语句发送给数据库服务器进行编译。对于重复执行的查询,这会导致性能问题,因为数据库服务器需要重复编译相同的查询语句。
  3. 可读性和维护性: 使用Statement时,SQL查询通常以字符串形式嵌入Java代码中,这可能导致SQL语句在代码中分散分布,降低代码的可读性和维护性。特别是对于复杂的SQL查询,代码的可读性将变得更差。
  4. 不支持参数化查询:Statement中,SQL查询通常是硬编码在Java代码中的,没有很好地支持参数化查询。参数化查询是一种更安全和高效的方式,它允许将参数值与SQL语句分开,避免了SQL注入风险,并且可以利用数据库的查询缓存。

预编译statement - PreparedStatement

使用preparedstatement与使用statement的流程是一致的,只是预编译的需要多加一部去传入参数。

1、简单例子演示预编译statement

用个简单的例子来演示一下。与statement没什么太大的区别,主要是创建传输sql的对象不一样,不然其他基本都是相同的,这里需要注意的是,在使用预编译statement的SQL语句需要用“?”占位符来替代动态数据。在创建PreparedStatement对象之后,需要根据下标进行给占位符赋值,顺序是从左往右,从1开始。最后执行与statement大致相同,单纯查询就是用executeQuery。最后得到的结果集可以直接解析。

/**
 * @Author: lyd
 * @Description: 使用预编译Statement
 * @Date: 2023/7/23
 */
public class JDBCPreparedStatement {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String name = "lyd";
        // 1、注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2、创建连接
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
        // 3、创建preparedstatement
        // 3.1、编写sql语句 不包含动态部分,动态值用占位符?来替代
        String sql = "SELECT * FROM tb_user WHERE username = ?";
        // 3.2、创建preparedstatement,传入动态值
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        // 3.3、占位符赋值: 从左向右,从1开始。set是有类型的,可以使用Object
        preparedStatement.setObject(1, name);
        // 4、发送sql  查询就用executeQuery
        ResultSet resultSet = preparedStatement.executeQuery();
        // 5、解析结果
        while (resultSet.next()) {
            System.out.println(resultSet.getString(1));
            System.out.println(resultSet.getString(2));
            System.out.println(resultSet.getString(3));
        }
        // 6、释放资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

2、进一步了解PreparedStatement

通过上面的简单实例,我们可以大致了解预编译statement无论是在安全上还是性能上都会比statement更胜一筹。我们进入PreparedStatement接口中可以看到他实际上是继承了Statement。
【JDBC系列】- 核心API之preparedstatement用法_第2张图片

官方也提供了一个例子,通过下标给占位符去赋值。这样就能够很好的解决SQL注入的风险。

1)、执行DML语句

使用预编译statement执行DML语句,操作步骤与查询是一致的,只是在发送SQL的时候使用的是executeUpdate,他返回的并不是结果集,而是行数,正如我们用Navicat执行时显示的几行受影响一样,成功执行了多少行数据就会返回多少行,当执行失败就会返回0。

代码如下

public class JDBCDMLPreparedStatement {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String name = "lyd";
        // 1、注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2、创建连接
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
        // 3、创建preparedstatement
        // 3.1、编写sql语句 不包含动态部分,动态值用占位符?来替代
        String sql = "INSERT INTO tb_user(id,username,address) VALUES (?,?,?)";
        // 3.2、创建preparedstatement,传入动态值
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        // 3.3、占位符赋值: 从左向右,从1开始。set是有类型的,可以使用Object
        preparedStatement.setObject(1, 3);
        preparedStatement.setObject(2, "opo");
        preparedStatement.setObject(3, "quanzhou");
        // 4、发送sql  DML语句使用
        int row = preparedStatement.executeUpdate();
        // 5、解析结果
        System.out.println(row + "行受影响");
        // 6、释放资源
        preparedStatement.close();
        connection.close();
    }
}

执行后,可以在数据库中看到新增了这条数据。
【JDBC系列】- 核心API之preparedstatement用法_第3张图片

2)、执行DQL语句

这次我们获取所有数据,并且遍历结果集,将每行的数据保存到一个链表当中,用map进行key-value进行映射。

注册驱动

还是一样的方法,我们采用反射的方式来注册驱动。

Class.forName("com.mysql.cj.jdbc.Driver");
创建连接
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
创建preparedstatement

因为不需要传入参数,也就没有使用占位符,然而这里也就不需要去对占位符进行赋值,只需要创建preparedstatement对象,将SQL语句放入就行。

// 编写sql语句 不包含动态部分,动态值用占位符?来替代
String sql = "SELECT * FROM tb_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
发送SQL
// 发送sql  查询就用executeQuery
ResultSet resultSet = preparedStatement.executeQuery();
解析结果集

我们需要将每行数据进行提取,并且提取列名作为key而列值作为value来进行类似映射封装,最后存在List中。在之前就知道了ResultSet包含了一个行的游标,通过next可以将游标移动到下一行。然后可以通过get方法,来根据列标签或者是下表来获取值。如果要达成我们的目的,可以直接自定义key名称,再通过get方法获取值。虽然这样可以做到,但是灵活度不高。而本次要学习的是灵活度相对高一点的方案。

我们看一下数据集,这有3行数据,每行有3列,我们可以通过next方法来做到移动游标获取行数据,那么列呢?我们要如何才能获得数据库的列名或者查询数据的列标签呢?
【JDBC系列】- 核心API之preparedstatement用法_第4张图片

ResultSetMetaData

ResultSet中有一个getMetaData()方法,它能够返回一个ResultSetMetaData对象,这个对象就是当前结果集列信息对象。我们通过源码可以看到这个列信息对象中包含了会话和字段等信息。

public class ResultSetMetaData implements java.sql.ResultSetMetaData {
    private Session session;
    private Field[] fields;
    boolean useOldAliasBehavior = false;
    boolean treatYearAsDate = true;
    private ExceptionInterceptor exceptionInterceptor;
    // ...
}

我们可以通过getColumnCount()方法获取这个结果集有多少个字段(列)。在package com.mysql.cj.jdbc.result.ResultSetMetaData#getColumnCount,实际上就是统计了 Field[] fields 这个字段数组的长度。

public int getColumnCount() throws SQLException {
    try {
        return this.fields.length;
    } catch (CJException var2) {
        throw SQLExceptionsMapping.translateException(var2, this.exceptionInterceptor);
    }
}

这样我们就可以在每行数据集中去遍历每列数据,这里需要注意的是,遍历的下标是从1开始到列长度数量。

获取列的信息就通过ResultSetMetaData对象获取,获取行相关信息就通过ResultSet对象获取,ResultSetMetaData能够获取列名或者列标签,建议使用列标签,因为有可能在查询的时候取了别名。

ResultSet resultSet = preparedStatement.executeQuery();
List<Map> list = new ArrayList<>();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 解析结果
while (resultSet.next()) {
    Map map = new HashMap<>();
    // 遍历数据
    for (int i = 1; i <= columnCount; i++) {
        // 通过下标获取列名 - 通过 ResultSetMetaData对象获取
        String columnLabel = metaData.getColumnLabel(i);
        // 通过下标获取列值 - 通过 ResultSet对象获取
        String value = resultSet.getString(i);
        map.put(columnLabel, value);
    }
    list.add(map);
}

运行之后的结果
在这里插入图片描述
ResultSetMetaData对象中还有许多的方法,如果是学习mybatis底层源码的时候,这些都是必不可少的资料。通过断点可以查看这个对象中的属性有哪些。这些后续会慢慢深入研究,并把学习成果分享出来。
【JDBC系列】- 核心API之preparedstatement用法_第5张图片

你可能感兴趣的:(JDBC,数据库,jdbc,java)