IDEA创建Maven项目并完成JDBC连接MySQL数据库详细步骤(JDBC中开启事务管理、ResultSe查询返回t对象、PreparedStatement预防SQL注入)

1、java操作数据库的流程

  1. 编写java代码
  2. Java代码将SQL发送到MySQL服务端
  3. MySQL服务端接收到SQL语句并执行该SQL语句
  4. 将SQL语句执行的结果返回给Java代码

2、编写代码的具体步骤

  1. 在maven中导入驱动包
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>

如果是第一次导入依赖包需要连接网络

导入单元测试包,方便测试

<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13.1</version>
   <scope>test</scope>
</dependency>
  1. 连接数据库的具体步骤
    注册jdbc驱动
// 1、注册JDBC驱动【其实在高版本的依赖中可以不写注册驱动】
// Class.forName("com.mysql.jdbc.Driver");

MySQL5之后的版本jdbc不需要注册

快速连接数据库

// 2、快速连接数据库
String url = "jdbc:mysql:///db2"; // 连接数据库服务器,并指定连接的数据库
String username = "root"; // 登录名称
String password = "1234"; // 登录密码
Connection conn = DriverManager.getConnection(url,username,password);

连接的方式:

原url写法:jdbc:/mysql:/127.0.0.1:3306/db2

在确保是连接本机的数据库并端口是3306的可以采取上面简写的方式连接

在数据库后面可以通过?接上配置项的参数键值对

定义sql语句

// 3、定义sql语句
String sql = "update account set money = 5000 where id = 1";

获取sql执行对象和执行sql语句

// 4、获取sql执行对象
Statement stmt = conn.createStatement();
// 5、执行sql语句
int count = stmt.executeUpdate(sql);// 返回受影响的行数
System.out.println("受影响的行数:"+count);

createStatement是一个普通的sql对象,不能防止sql注入,后期会使用到prepareStatement()来获取执行对象

释放资源

// 6、释放资源
stmt.close();
conn.close();
  1. 连接安全提示解决
    连接提示
Wed Mar 23 07:25:44 CST 2022 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

原因:不建议在没有服务器身份验证的情况下建立 SSL 连接

解决方式:连接数据库表后面加上如下参数:useSSL=false

String url = "jdbc:mysql:///db2?useSSL=false"; // 连接数据库服务器,并指定连接的数据库

3、JDBC事务管理

事务管理开启的三步骤:
  1. 开启事务:NEGIN;或者 START TRANSACTION;
  2. 提交事务:COMMIT;
  3. 回滚事务: ROLLBACK;

MySQL默认是自动提交事务

Connection几口中定义了3个对应事务的方法:
开启事务方法
Connection.setAutoCommit(false);

参与autoCommit 表示是否自动提交事务,true表示自动提交事务,false表示手动提交事务。而开启事务需要将该参数设为为false。

提交事务方法
Connection.commit();
回滚事务方法
Connection.rollback();

事务回滚可以放在捕获报错的try()catch中,如果报错了就回滚事务

事务的具体演示方法如下:

代码与前面的一样,就添加了事务的三句代码和加一个捕获来完成

前面提到的Connection是一是类,里面具有事务的三个方法

/**
     * 2、事务提交示例
     */
    @Test
    public void test2() throws Exception {
        // 1、注册驱动,这里不注册了
        // 2、连接数据库【声明了一个测试使用私有的全局连接账号和密码】
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        // 3、定义执行sql语句
        String sql = "update account set name = '修改的',money = 0 where id = 2";
        String sql2 = "update account set name = '修改的11',money = 0 where id = 11";
        // 4、获取执行对象
        Statement statement = conn.createStatement();
        // ---------- 开启事务提交执行语句 ----------
        try {
            // 开启事务
            conn.setAutoCommit(false);
            // 5、执行sql语句
            int count = statement.executeUpdate(sql);
            System.out.println(count);
            // ------ 制造程序报错 ------
            System.out.println(3/0);
            int count2 = statement.executeUpdate(sql2);
            System.out.println(count2);
            // 提交事务
            conn.commit();
        }catch (Exception e){
            // 事务回滚
            conn.rollback();
            System.out.println("程序出错了:"+e.getMessage());
        }
    }

程序执行完成,语句报错,返回受受影响行数为1,后面的代码没有执行到,遇到报错被拦截了,sql语句会被事务拦截回来,再次查看数据库的信息是没有被修改的,这就是事务的重要性

4、Statement中的两个执行SQL的方法

根据sql语句的不同,JDBC给出了两个不同的执行语句的方法

执行DDL、DML修改添加等语句的:executeUpdate()

执行DQL查询语句的:executeQuery()

上面的两个测试代码都使用了executeUpdate()来执行DML修改语句,这里不再演示

使用executeQuery()执行DDL删除数据库语句

一般不会使用java代码来完成DDL语句

/**
     * 删除数据库语句,不要执行
     * @throws Exception
     */
    @Test
    public void deltest() throws Exception {
        // 1、连接数据库
        Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        // 2、定义sql语句
        String sql = "drop database db2"; // 删除db2数据库
        // 3、获取执行对象
        Statement statement = connection.createStatement();
        int i = statement.executeUpdate(sql);
        if(i>0){
            System.out.println("数据修改成功");
        }else {
            System.out.println("数据修改失败");
        }
        // 4、释放资源
        statement.close();
        connection.close();
    }
executeQuery()执行DQL查询数据库语句
/**
     * 查询数据库语句
     * 返回ResultSet对象
     *  通过ResultSet对象通过的next()判断是否有数据
     *  通过ResultSet对象通过的getXXX()方法获取具体的列数据
     *  XXX代表列的数据类型
     * @throws Exception
     */
    @Test
    public void querytest() throws Exception {
        // 1、连接数据库
        Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        // 2、定义sql语句
        String sql = "select * from account"; // 删除db2数据库
        // 3、获取执行对象
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);
        // 返回的ResultSet对象提供了next()方法检验释放有数据
        while (resultSet.next()) {
            // 通过  getXxx(参数)获取查询的数据
            // 可以指定查询列的字段名称或对于的列号查询数据
            // int id = resultSet.getInt(1);
            int id = resultSet.getInt("id");
            String name = resultSet.getString("name");
            String money = resultSet.getString("money");
            System.out.print(id + " ");
            System.out.print(name + " ");
            System.out.print(money + "");
            System.out.println();
        }
        // 4、释放资源
        resultSet.close();
        statement.close();
        connection.close();
    }

5、查询返回ResultSet对象

上面的一段代码就是使用了executeQuery()方法来执行SQL的查询语句,返回的是一个ResultSet对象

如何判断和获取ResultSet对象中的数据

ResultSet.next():

判断当前行是否为有效行,检验是否查询到数据

有效返回:true

ResultSet.getXXX():获取表中具体列的数据

XXX:代表了数据类型,例如:getInt(参数) ; getString(参数)

参数:

  • int类型的参数:代表了列的编号,从1开始
  • String类型的参数:代表了列的名称
  • 上面的查询代码中演示了,一个列编号和列字段名称获取的id信息

6、PreparedStatement预防SQL注入

作用:预编译SQL语句并执行:可以预防SQL注入问题

什么是SQL注入:

SQL注入是通过操作输入来修改事先定义好的SQL语句,用以达到执行代码对服务器进行攻击的方法。

模拟SQL注入问题代码

在tb_user数据表中正确的登录名和密码是

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BM1Anqnm-1648004804469)(image/image-20220323104500031.png)]

/**
     * 模拟SQL注入问题演示
     * @throws Exception
     */
    @Test
    public void testLogin() throws  Exception {
        //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        // 接收用户输入 用户名和密码
        String name = "sjdljfld";
        String pwd = "' or '1' = '1";
        String sql = "select * from tb_user where username = '"+name+"' and password = '"+pwd+"'";
        // 获取stmt对象
        Statement stmt = conn.createStatement();
        // 执行sql
        ResultSet rs = stmt.executeQuery(sql);
        // 判断登录是否成功
        if(rs.next()){
            System.out.println("登录成功~");
        }else{
            System.out.println("登录失败~");
        }

        //7. 释放资源
        rs.close();
        stmt.close();
        conn.close();
    }
注入问题解析

从上面语句可以看出条件 username = 'sjdljfld' and password = '' 不管是否满足,而 or 后面的 '1' = '1' 是始终满足的,最终条件是成立的,就可以正常的进行登陆了。

PreparedStatement设置参数和使用方法

使用?占位符来代替传入的参数

String sql = "select * from tb_user where username = ? and password = ?";

通过Connection对象获取操PreparedStatement(sql)方法,并除闯入sql语句

PreparedStatement ps = conn.prepareStatement(sql);

后面调用executeUpdate()或executeQuery()执行方法的时候,就不再需要传递sql语句了,因为prepareStatement()获取SQL语句执行对象时已经对SQL语句进行预编译了。

给?占位符赋值

PreparedStatement.setXXX(参数1,参数2)

XXX:数据类型:ps.setString(1,name);

参数解说:

参数一:是?的编号位置,从1开始

参数二:是对?赋的值

改进上面的SQL注入问题
/**
     * 使用PreparedStatement改进代码防止SQL注入问题
     * @throws Exception
     */
    @Test
    public void testLogin2() throws  Exception {
        //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        // 接收用户输入 用户名和密码
        String name = "sjdljfld";
        String pwd = "' or '1' = '1";
        // String sql = "select * from tb_user where username = '"+name+"' and password = '"+pwd+"'";
        String sql = "select * from tb_user where username = ? and password = ?";
        // 获取stmt对象
        // Statement stmt = conn.createStatement(); // 不嫩防止SQL注入问题
        PreparedStatement ps = conn.prepareStatement(sql);
        // 对?占位符进行赋值
        ps.setString(1,name);
        ps.setString(2,pwd);
        // 执行sql
        ResultSet rs = ps.executeQuery(); // 使用PreparedStatement就不需要再传递参数了,上面的语句已经预编译代码了
        // 判断登录是否成功
        if(rs.next()){
            System.out.println("登录成功~");
        }else{
            System.out.println("登录失败~");
        }

        //7. 释放资源
        rs.close();
        ps.close();
        conn.close();
    }

执行上面语句就可以发现不会出现SQL注入漏洞问题了。那么PreparedStatement又是如何解决的呢?它是将特殊字符进行了转义,转义的SQL如下:

select * from tb_user where username = 'sjdljfld' and password = '\'or \'1\' = \'1'
PreparedStatement原理

将敏感字进行转义

Java代码操作数据库流程如图所示:

  • 将sql语句发送到MySQL服务器端
  • MySQL服务端会对sql语句进行如下操作
  • 检查SQL语句
    检查SQL语句的语法是否正确。
  • 编译SQL语句。将SQL语句编译成可执行的函数。
    检查SQL和编译SQL花费的时间比执行SQL的时间还要长。如果我们只是重新设置参数,那么检查SQL语句和编译SQL语句将不需要重复执行。这样就提高了性能。
  • 执行SQL语句

接下来我们通过查询日志来看一下原理。

  • 开启预编译功能
    在代码中编写url时需要加上以下参数。而我们之前根本就没有开启预编译功能,只是解决了SQL注入漏洞。
useServerPrepStmts=true

参数后面添加的代码

String url = "jdbc:mysql:///db2?useSSL=false&useServerPrepStmts=true";
  • 配置MySQL执行日志(重启mysql服务后生效)
    在mysql配置文件(my.ini)中添加如下配置
log-output=FILE
general-log=1
general_log_file="D:\mysql.log"
slow-query-log=1
slow_query_log_file="D:\mysql_slow.log"
long_query_time=2

小结:

  • 在获取PreparedStatement对象时,将sql语句发送给mysql服务器进行检查,编译(这些步骤很耗时)
  • 执行时就不用再进行这些步骤了,速度更快
  • 如果sql模板一样,则只需要进行一次检查、编译