首页 > 技术文章 > JDBC第二部分—statment和preparedStatement

cangfengwork 2021-04-10 22:37 原文

statment和preparedStatement

Statement使用的注意事项

statement的作用是执行一段静态的sql语句,并返回处理的结果集对象。但是statement存在一定的弊端:

①sql语句需要传递参数时,我们需要对sql语句进行拼接,这样会很麻烦,影响我们的开发效率。

②使用statement执行sql语句时会存在sql注入问题,简单来说就是利用没有对用户输入的数据进行检查,利用非法的sql语句完成恶意行为的做法

下面写了一个简单的登录例子,用来测试statement存在的sql注入问题。

正常访问数据库时:

 @Test
    public void queryDataByStatement()  {
        /*
        简单的登录模块测试statement的弊端
         */
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入用户账号:");
        String userNum = scanner.nextLine();
        System.out.print("请输入用户密码:");
        String password = scanner.nextLine();

        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            //1.获取数据库的连接:使用自定义工具类
            connection = MyJDBCUtils.getConnection();
            //2.创建一个statement实例
            statement = connection.createStatement();
            //3.创建sql语句:此处需要对sql语句进行拼串操作,略微麻烦
            String sql="select user,password from user_table where user='"+userNum+"' and password='"+password+"'";
            //4.执行sql语句
            resultSet = statement.executeQuery(sql);
            //5.对返回结果进行简单处理
            if (resultSet.next())
                System.out.println("登录成功!!!");
            else
                System.out.println("登录失败!!!");
            //6.关闭数据库的连接,此时statement和结果集也需要被关闭:使用自定义工具类
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            MyJDBCUtils.closeConnection(connection,statement,resultSet);
        }

    }

 

返回的结果是正常的:

 


 

当恶意访问数据库时:


@Test
public void queryDataByStatement() {
/*
简单的登录模块测试statement的弊端
*/
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户账号:");
String userNum = scanner.nextLine();
System.out.print("请输入用户密码:");
String password = scanner.nextLine();

Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;

try {
//1.获取数据库的连接:使用自定义工具类
connection = MyJDBCUtils.getConnection();
//2.创建一个statement实例
statement = connection.createStatement();
//3.创建sql语句:此处需要对sql语句进行拼串操作,略微麻烦

String sql="select user,password from user_table where user = '"+userNum+" 'and password = '"+password+"'";
//4.执行sql语句
resultSet = statement.executeQuery(sql);
//5.对返回结果进行简单处理
if (resultSet.next())
System.out.println("登录成功!!!");
else
System.out.println("登录失败!!!");
//6.关闭数据库的连接,此时statement和结果集也需要被关闭:使用自定义工具类
} catch (Exception e) {
e.printStackTrace();
} finally {
MyJDBCUtils.closeConnection(connection,statement,resultSet);
}

执行结果为:此时账号和密码明显不对,但是却能登陆成功。

 之所以出现这种情况,是因为statement没有对sql语句进行事先的编译,我们传入什么,它就会向数据库发送什么数据,当账号和密码是上图中的情况时,sql语句实际为下图的情况,这个就叫做sql注入

 

 

 


PreparedStatement使用注意事项

为了解决statement中sql注入的问题,我们需要使用preparedStatement来替换原有的statement。

  preparedStatement是statement的一个子接口,它的好处是可以对sql语句进行预编译,在创建preparedStatement实例时已经知道了自己要执行的sql语句是什么

使用preparedStatement完成对数据库的增删改查操作

1.使用prepareStatement向user表中添加一条数据

@Test
    public void addUserByPre()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库的连接
            connection = MyJDBCUtils.getConnection();
            //2.创建sql语句:此时数据库中设计的主键id是自增的,我们可以不用主动添加
            //?此时代表占位符,表明你将要传递的参数,有几个?代表需要传递几个参数
            String sql="insert into `user`(name,password,address,phone) values(?,?,?,?)";
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.注入占位符(
            // 两个参数,第一个为需要注入的占位符的下标,第二个参数为具体注入的内容.这里需要注意的是下标是从1开始的而不是0)。
            preparedStatement.setString(1,"王宝强");
            preparedStatement.setString(2,"123456");
            preparedStatement.setString(3,"河北省秦皇岛市");
            preparedStatement.setString(4,"12345678910");
            //5.执行相关操作
             preparedStatement.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭相应连接
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }

    }

2. 使用preparedStatement修改user表中的某条数据

 @Test
    public void updateUserByPre()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();
            //2.创建sql语句
            String sql="update user set name = ? where id = ?";
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.填充占位符
            preparedStatement.setString(1,"许三多");
            preparedStatement.setInt(2,8);
            //5.执行操作
            int i = preparedStatement.executeUpdate();
            if (i != 0)
                System.out.println("修改成功");
            else
                System.out.println("修改失败");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭资源
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }

    }

3.使用preparedStatement删除user表中的一条数据

  @Test
    public void deleteUserByPre(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            try {
                //1.获取数据库连接
                connection = MyJDBCUtils.getConnection();
                //2.创建sql语句
                String sql="delete from user  where id = ?";
                //3.创建preparedStatement对象
                preparedStatement = connection.prepareStatement(sql);
                //4.填充占位符
                preparedStatement.setInt(1,6);
                //5.执行操作
                int i = preparedStatement.executeUpdate();
                if (i != 0)
                    System.out.println("删除成功");
                else
                    System.out.println("删除失败");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //6.关闭资源
                MyJDBCUtils.closeConnection(connection,preparedStatement);
            }
    }

4.观察代码可以看出来,增删改三种方法的代码是具有一定的重复性的,唯一的区别无非就是sql语句和占位符的不同,因此我们可以考虑将三种方法封装为同一个方法,调用的时候只需要传递sql语句和占位符即可。代码如下(可自行测试,这里就不再写测试代码了)

 public static void updateDataBase(String sql,Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库的连接
            connection = getConnection();
            //2.创建sql语句,此步骤可直接使用传递进来的sql语句
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.填充占位符
        /*
            1.首先要获取占位符的个数,因为可变形参的个数就是占位符的个数,所以只需要获取args的长度即可
            2.填充占位符,使用for循环来做,需要注意的是下标的问题
         */
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //5.执行操作
            int i = preparedStatement.executeUpdate();
            if (i != 0)
                System.out.println("此次操作成功!");
            else
                System.out.println("此次操作失败!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭资源
            closeConnection(connection,preparedStatement);
        }

    }

5.使用preparedStatement查询user表中的一条记录(查询和增删改是不同的,因为查询需要有返回的结果集)

  public User queryUser(String sql,Object ...args)  {
        Connection connection = null;
        PreparedStatement prepareStatement = null;
        ResultSet resultSet = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();
            //2.创建sql语句

            //3.创建preparedStatement对象
            prepareStatement = connection.prepareStatement(sql);
            //4.填充占位符
            for (int i = 0; i < args.length; i++) {
                prepareStatement.setObject(i+1,args[i]);
            }
            //5.执行操作
            resultSet = prepareStatement.executeQuery();
            //6.将查询出来的数据封装成为一个对象
            //1.获取一个元数据对象
            ResultSetMetaData metaData = resultSet.getMetaData();
            //2.通过元数据对象来获取该条数据中一共有多少列
            int columnCount = metaData.getColumnCount();
            if (resultSet.next()){
                /*
                resultSet.next()有些类似与迭代器中的hashNext()和next()的结合体
                在迭代器中,hasNext()的作用是判断下一个位置是否为空,next()如果下一个位置不为空,指针下移并且返回当前对象,如果为空,则结束操作
                而resultSet.next()的作用是判断下一个位置是否为空,并且指针下移,返回的是Boolean值
                 */
                 //3.创建一个对象实体
                User user = new User();
                //如何将数据封装进一个JavaBean中呢?此时并不知道取出的元素具体是什么类型的!
                //在resultSet中提供了一个方法用来获取查询到的元数据(元数据:修饰查询出来数据的数据,可以参考元注解的概念),
                // 4.使用元数据来获取当前这一条数据的每一列的列名和对应的列值
                for (int i = 0; i < columnCount; i++) {
                    String columnName = metaData.getColumnName(i + 1);
                    Object columnValue = resultSet.getObject(i + 1);

                    //5.使用反射技术动态的为bean对象中的属性赋值
                    Field declaredField = User.class.getDeclaredField(columnName);
                    declaredField.setAccessible(true);
                    declaredField.set(user,columnValue);
                }
                //System.out.println(user);
                return user;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭资源
            MyJDBCUtils.closeConnection(connection,prepareStatement,resultSet);
        }
        return null;
    }

 这个时候需要提供一个对应的JavaBean实例

6.同样的,我们也可以封装一个函数用来获取不同的表中的单条数据

   public static <T> T getBeanByPre(Class<T> clazz,String sql,Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //1.获取到数据库的连接
            connection = getConnection();
            //2.创建一个preparedStatement实例
            preparedStatement = connection.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //4.执行操作
            resultSet = preparedStatement.executeQuery();
            //5.获取查询记录的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            //6.获取查询记录中的列数
            int columnCount = metaData.getColumnCount();
            if (resultSet.next()){
                //7.使用反射创建一个bean是咧
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //8.获取到每一列的别名
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //9.获取到每一列的值
                    Object columnValue = resultSet.getObject(i + 1);

                    //10.使用反射为bean中的属性赋值
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //11.关闭资源
            closeConnection(connection,preparedStatement,resultSet);
        }
        return null;
    }

7.我们也可以封装一个函数用来获取不同的表中的多条数据

public static <T>List<T> getBeanListByPre(Class<T> clazz, String sql, Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //1.获取到数据库的连接
            connection = getConnection();
            //2.创建一个preparedStatement实例
            preparedStatement = connection.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //4.执行操作
            resultSet = preparedStatement.executeQuery();
            //5.获取查询记录的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            //6.获取查询记录中的列数
            int columnCount = metaData.getColumnCount();
            //7.创建list集合
            ArrayList<T> list = new ArrayList<>();
            while (resultSet.next()){
                //7.使用反射创建一个bean实例
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //8.获取到每一列的别名
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //9.获取到每一列的值
                    Object columnValue = resultSet.getObject(i + 1);

                    //10.使用反射为bean中的属性赋值
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //11.关闭资源
            closeConnection(connection,preparedStatement,resultSet);
        }
        return null;
    }

 之所以preparedStatement可以解决sql注入问题,是因为它的预编译sql语句的功能,在我们生成preparedStatement对象时,已经使用过了sql语句。在占位符还没有填充之前,它就已经对sql语句进行了解析。对于刚开始的登录测试来说,创建preparedStatement对象时,sql语句表示的就是user = ?and password = ?,无论传入什么值,它都会认为是user和password的值。以此避免了sql注入问题

 preparedStatement同时还可以操作Blob类型的数据,可以更高效的实现批量操作

 


 

preparedStatement和statement的对比

1.代码的可读性和可维护性:

在statement中,sql语句需要我们主动的去拼接,这样在可读性的体验上是非常差的,同时维护起来也是十分的不方便。在preparedStatement中,对于参数的传递我们使用占位符来替代,这样大大的提高了代码的可读性,同时维护起来也不容易出错。

2.preparedStatement能最大可能地提高性能:

(1)DBServer会对预编译的语句提供性能的优化。因为预编译语句有可能会被重复的调用,所以语句在被DBServer的编译器编译之后的执行代码会被缓存下来,那么下次调用时,只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的预编译语句中就会得到执行。

(2)在statement语句中,即使是相同操作但是因为数据内容不一样,所以整个语句本身不能匹配,不存在缓存语句的意义,这样每执行一次都要对传入的语句编译执行一次。

3.preparestatement还可以有效的防止sql注入问题。

 


批量插入的对比

使用statement进行批量插入

 @Test
    public void statementInsert() {
        Connection connection = null;
        Statement statement = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();
            //2.创建statement对象
            statement = connection.createStatement();
            //3.模拟演示批量插入操作

            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 2000; i++) {
                //4.提供sql语句
                String sql = "insert into goods(name) values('name-"+i+"')";
                statement.executeUpdate(sql);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("statement批量插入所需要的时间为:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            MyJDBCUtils.closeConnection(connection,statement);
        }

    }

所花费的时间为:

 

 

 

使用prepareStatement实现批量插入操作

 @Test
    public void preparedStatementInsert()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();
            //2.提供sql语句
            String sql = "insert into goods(name) values(?)";
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                preparedStatement.executeUpdate();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement执行批量插入所需要的时间为:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花费的时间为

 

 

 

使用batch相关操作提高批量插入的效率:mysql默认是关闭批处理的,我们需要在url中添加参数来开启批处理的操作。?rewriteBatchedStatements=true

 @Test
    public void preparedStatementInsert1()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();
            //2.提供sql语句
            String sql = "insert into goods(name) values(?)";
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i <= 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                //6.在填充占位符之后不立刻提交,而是积累到一定数量之后一起提交
                    //1.攒sql
                preparedStatement.addBatch();

                if (i % 500 == 0)
                {
                    //2.提交积攒的500条sql语句
                    preparedStatement.executeBatch();

                    //3.清空batch
                    preparedStatement.clearBatch();
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement中使用batch执行批量插入所需要的时间为:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花费的时间为:

 

 

 

批量提交的方式四:默认情况下,数据库中的DML操作每执行一次就会自动提交,也就是说在上述的第三种方式中,每500次都会重新执行一次insert,意味着每500次都会提交一次数据然后将数据写入到数据库,写入操作也是会花费一定的时间的,如果数据量小可能看不出来,但是如果是百万条数据的话,时间方面的差异就体现出出来了。对此,我们可以关闭数据库的默认提交操作,等把数据全部读入之后在进行统一的提交

  @Test
    public void preparedStatementInsert2()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.获取数据库连接
            connection = MyJDBCUtils.getConnection();

            //关闭数据库的自动提交功能
            connection.setAutoCommit(false);

            //2.提供sql语句
            String sql = "insert into goods(name) values(?)";
            //3.创建preparedStatement对象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i <= 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                //6.在填充占位符之后不立刻提交,而是积累到一定数量之后一起提交
                //1.攒sql
                preparedStatement.addBatch();

                if (i % 500 == 0)
                {
                    //2.提交积攒的500条sql语句
                    preparedStatement.executeBatch();

                    //3.清空batch
                    preparedStatement.clearBatch();
                }
            }
            //对数据进行统一的提交
            connection.commit();
            
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement中使用batch执行批量插入所需要的时间为:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花费的时间为:

 

 

 

推荐阅读