MySQL系列之(三)JDBC持久层访问相关解决方案

JDBC持久层访问相关解决方案

1.1 JDBC的思想和价值

JDBC是Java给出的一套基于面向接口的,针对持久层访问的解决方案。这些接口按照使用逻辑顺序有Driver/Connection/Statement/ResultSet(java.sql包下),分别解决驱动注册、连接、执行语句、结果集,而各大数据库厂商基于这套接口各自针对自己的数据库进行实现,是一个标准的面向接口编程的经典案例。

Java应用开发者同样面向接口去编程,通过接口规范方法进行数据库的CRUD操作,无需关心实现类细节,当然需要引入对应的数据库厂商的实现类,即jar包。从以上可以真切的看出了面向接口编程的巨大优势:

  • 实现了调用者(码农)与被调用者(数据库厂商)解耦
  • 增强了java应用对持久层实现的可移植性,同一套应用可以轻松的跨数据库运行
  • 如果在项目团队中,那就很大提高了分工协作,提升团队开发效率

1.2 JDBC方式实现Mysql数据库访问以及原理详解

1.2.1 原理详解

  1. 注册驱动
  • 数据库厂商实现Driver接口
  • 应用程序通过 Class.forName()把Driver实现类加载到内存
  • Driver实现类中编写了静态代码块,把驱动注册到驱动管理类
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

重要说明:

DriverManager,该类类似工具类,各数据库厂在实现Driver类时,会借助驱动管理类的静态注册方法将驱动注册上去,随后开发人员可以通过驱动管理类静态方法获取数据库连接,从这个角度,可以总结JDBC就是
1 + N的解决方案,1-DriverManager工具类,N-一套接口。

  1. 获取连接
  • 数据库厂商实现Connection接口
  • 通过DriverManager静态方法获取连接DriverManager.getConnection(url,user,password)
  • url:JDBC规定了url的格式,三部分组成:jdbc协议名,mysql子协议名,库连接(数据库厂商提供规范),其中IP代表目标主机、3306是数据库端口、test是数据库database、?问号后面可以跟参数,characterEncoding是指定客户端字符集编码,防止中文乱码。

样例:

jdbc:mysql://IP:3306/test?characterEncoding=utf-8

1.2.2 代码实现

import java.sql.*;

/**
 * @author fengqingyang
 * @create 2022-05-26 8:58
 */
public class JDBCTest {
    public static void main(String[] args) {

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

        try {
            //1 注册驱动
            Class.forName("com.mysql.jdbc.Driver");

            // 2 获取连接
            String url = "jdbc:mysql://IPxxx:3306/test?characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, "root", "root");

            // 3 获取执行平台
            statement = connection.createStatement();

            // 4 执行并处理结果
            String name = "张三";
            String sql = "select * from citizen where cust_name = '" + name + "'";
            System.out.println("拼接的sql=" + sql);
            resultSet = statement.executeQuery(sql);
            while (resultSet.next()) {
                String cust_name = resultSet.getString("cust_name");
                String cust_cert = resultSet.getString("cust_cert");
                System.out.println("cust_name=" + cust_name + ",cust_cert=" + cust_cert);
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5 关闭资源,遵循先开后关原则
            if(resultSet != null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(statement != null){
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(connection != null){
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

打印结果:
在这里插入图片描述

1.3 关于SQL注入的理解和解决方式

1.3.1 原理详解&代码演示

  • SQL注入原因:

首先能明确观察到上面JDBC查询代码中,SQL是以字符串拼接的形式完成的,这就带来了隐患,加上SQL入参由外部或者前台传入,若传入的字符串参数修改了原本SQL的语义,就造成了SQL注入。

代码演示:

 // 4 执行并处理结果
            //String name = "张三"; //正常的入参
            String name = "不存在的名字' or '1'='1";  // 造成SQL注入的入参
            String sql = "select * from citizen where cust_name = '" + name + "'";
            System.out.println("拼接的sql=" + sql);
            resultSet = statement.executeQuery(sql);

SQL注入导致异常查询结果:
在这里插入图片描述

  • SQL注入的解决:

知道注入的原因是字符串拼接,那么就需要规避这个原因来规避SQL注入,java提供的预编译接口类PrepareStatement(Statement的子接口,同样由数据库厂商提供实现)可以解决SQL注入问题,该接口中执行sql方法(prepareStatement)会把传入的sql进行预编译,同事传入的sql不存在字符串拼接,而是通过占位符【?】来代替传参,随后执行时,给预编译的SQL传递参数,无论如何都不会改变SQL原本业务逻辑或者语义,注入问题也就解决了,同时预编译也会提高执行效率,同样的SQL一次编译,多次执行(避免多次编译)。

备注:
编译是数据库SQL执行前需要完成数据库内部工作,细分来说需要经过分析器(语法语义判断分析)、优化器(生成执行计划)、执行器(调用存储引擎进行查询),执行器之前的工作视为是SQL编译。

代码演示:

 @Test
    public void testSQLInjection() {
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            //1 注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2 获取连接
            String url = "jdbc:mysql://linux124:3306/test?characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, "root", "root");
            // 3 获取执行平台
            String sql = "select * from citizen where cust_name = ?";
            //进行sql预编译
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            // 4 执行并处理结果
            //String name = "张三"; //正常的入参
            String name = "不存在的名字' or '1'='1";  // 造成SQL注入的入参
            //设置参数
            preparedStatement.setString(1, name);
            System.out.println("占位符的sql=" + sql);
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                String cust_name = resultSet.getString("cust_name");
                String cust_cert = resultSet.getString("cust_cert");
                System.out.println("cust_name=" + cust_name + ",cust_cert=" + cust_cert);
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5 关闭资源,遵循先开后关原则
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

结果查询(已规避SQL注入):
在这里插入图片描述

1.4 JDBC的事务支持

1.4.1 原理详解

  • 为什么需要事务?

由于实际业务中,我们可能需要进行一连串/多次数据操作,并且需要他们同时执行成功,例如银行转账业务,订单下单与库存数量等等,这其实就是业务的原子性、一致性,从数据库层面给出了解决方案,那就是事务控制,事务的特点就是ACID,原子性,一致性,隔离性、持久性。

  • 使用事务也需要解决的问题?

由于事务之间针对共享数据的读写会存在脏读、不可重复度等问题。脏读就是A事务读取了B事务未提交的数据,不可重复度就是A事务中第一次读取数据结果与后续读取的结果不同(当然在A事务未提交期间,有其他事务对共享数据做了修改导致)。为此数据库提供了事务隔离的四个级别,这里我们只需要熟悉两个即可(其他两个基本用不到):

  1. 读已提交:这是oracle等采用的默认级别,可以解决脏读;
  2. 可重复读:Myql的默认级别,可以解决脏读和不可重复读;

代码演示:读已提交的比较容易理解,这里只演示下可重复读。

  • 测试数据查看,状态=1的是两条数据:
    在这里插入图片描述
  • 窗口1中:开启事务(利用begin开启)后,进行数据查询(不进行commit)
    在这里插入图片描述
  • 窗口2中:开启事务,修改一条数据状态为0,然后提交事务(这里其实可以不用手动开启再提交,为了演示效果才这样),在窗口2中已经可以查看到一条记录被修改为state=0;
    在这里插入图片描述
    此时回到窗口1再次查看(窗口1此时未提交事务),可以看到此时在该事务中保持了可重复度。
    在这里插入图片描述
    然后在窗口1提交事务,再次查看,此时由于事务已经提交,所以能查看到窗口2(事务2)之前修改过的数据。
    在这里插入图片描述

1.4.2 代码演示

JDBC的实现比较简单,就是调用connection的事务方法,需要事务控制的代码之前开启事务,正常结束之后提交事务,异常时回滚事务,详看代码注释:

    @Test
    public void testJDBCTransaction(){
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            //1 注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2 获取连接
            String url = "jdbc:mysql://linux124:3306/test?characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, "root", "root");
            // 3 获取执行平台
            String sql = "update citizen set cust_name = ? where cust_name = ?";
            //进行sql预编译
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            // 4 执行并处理结果
            //设置参数
            preparedStatement.setString(1, "王五");
            preparedStatement.setString(2, "张三");

            // 开始事务: 关闭默认提交就是开启事务
            connection.setAutoCommit(false);
            preparedStatement.executeUpdate();
            // 正常提交
            connection.commit();

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            // 异常时,事务回滚
            try {
                connection.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        } finally {
            // 5 关闭资源,遵循先开后关原则
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

后续,我们在交流分享数据库连接池和DBUtils框架,感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值