掘金 后端 ( ) • 2024-04-14 17:56

[学习地址](【尚硅谷2024最新JDBC教程 | jdbc基础到高级一套通关!】 https://www.bilibili.com/video/BV1Tx421S7HZ/?p=34&share_source=copy_web&vd_source=1a47fe5dfa3ae49f76d25e6238dd7d03)

建立项目如图: image.png

并执行sql语句:

CREATE DATABASE rainsoul;  
  
use rainsoul;  
  
create table t_emp  
(  
  emp_id     int auto_increment comment '员工编号' primary key,  
  emp_name   varchar(100)  not null comment '员工姓名',  
  emp_salary double(10, 5) not null comment '员工薪资',  
  emp_age    int           not null comment '员工年龄'  
);  
  
insert into t_emp (emp_name, emp_salary, emp_age)  
values ('andy', 777.77, 32),  
       ('大风哥', 666.66, 41),  
       ('康师傅', 111, 23),  
       ('Gavin', 123, 26),  
       ('小鱼儿', 123, 28);

2.快速入门

查询数据库所有信息:

package com.rainsoul.base;  
  
import java.sql.*;  
  
public class JDBCQuick {  
    /**  
     * 主函数:演示从MySQL数据库中查询员工信息并打印。  
     * @param args 命令行参数(未使用)  
     * @throws ClassNotFoundException 如果MySQL JDBC驱动未找到,则抛出此异常  
     * @throws SQLException 如果数据库操作出现错误,则抛出此异常  
     */  
    public static void main(String[] args) throws ClassNotFoundException, SQLException {  
        // 注册MySQL JDBC驱动  
        Class.forName("com.mysql.cj.jdbc.Driver");  
  
        // 获取到数据库的连接  
        Connection connection = DriverManager  
                .getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");  
  
        // 创建PreparedStatement对象,用于执行预编译的SQL查询  
        PreparedStatement preparedStatement = connection  
                .prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp");  
  
        // 执行查询语句,获取结果集  
        ResultSet resultSet = preparedStatement.executeQuery();  
  
        // 处理查询结果,将每条员工信息打印出来  
        while (resultSet.next()) {  
            int empId = resultSet.getInt("emp_id");  
            String empName = resultSet.getString("emp_name");  
            String empSalary = resultSet.getString("emp_salary");  
            int empAge = resultSet.getInt("emp_age");  
            System.out.println(empId + "\t" + empName + "\t" + empSalary + "\t" + empAge);  
        }  
  
        // 释放资源,关闭ResultSet、PreparedStatement以及Connection  
        resultSet.close();  
        preparedStatement.close();  
        connection.close();  
    }  
}

查询结果: image.png

3.核心API流程的理解

3.1注册驱动

Class.forName("com.mysql.cj.jdbc.Driver");
  • 在 Java 中,当使用 JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够识别并与特定的数据库进行交互。

  • 从JDK6开始,不再需要显式地调用 Class.forName() 来加载 JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。

在Java程序中执行 Class.forName("com.mysql.cj.jdbc.Driver") 这一行代码时,以下底层过程会依次发生: 类加载:

  1. JVM接收到这个字符串字面量,根据它来查找并加载指定名称的类。类加载是通过类加载器(ClassLoader)体系实现的。通常情况下,系统类加载器(System ClassLoader)负责加载用户类路径(classpath)上的类。类加载包括三个阶段:加载(Loading)、验证(Verification)、准备(Preparation)。在这个过程中,JVM会查找指定类的.class文件(对于MySQL JDBC驱动来说,通常位于jar包中),将其字节码数据读入内存,并进行必要的安全性和正确性检查。
  2. 类初始化:在类加载完成后,如果该类尚未被初始化,JVM会触发类的初始化过程。类初始化包括分配静态变量内存、执行静态初始化块(static {})中的代码等步骤。对于 com.mysql.cj.jdbc.Driver 类,其内部通常包含一个静态初始化块或静态方法,在类初次被加载时会被自动执行。这个初始化逻辑的核心任务是向Java的JDBC驱动管理器(java.sql.DriverManager)注册该驱动。
  3. 驱动注册:com.mysql.cj.jdbc.Driver 类(或者其他MySQL JDBC驱动类,如旧版的 com.mysql.jdbc.Driver)会实现 java.sql.Driver 接口。按照JDBC规范,实现该接口的类需要在初始化时向 DriverManager 注册自己。注册通常是通过调用 DriverManager.registerDriver() 方法完成的,但在MySQL JDBC驱动中,可能采用更现代的Service Provider Interface (SPI)机制,即在类路径下的 META-INF/services/java.sql.Driver 文件中声明该驱动类。这样,当JDBC API尝试加载驱动时,会自动发现并加载这个文件中列出的实现类。注册过程实质上是将驱动类的一个实例(通常是一个单例或无状态对象)添加到 DriverManager 内部的驱动列表中。这样,当后续调用 DriverManager.getConnection() 方法时,DriverManager 就知道有哪些可用的驱动可以用来尝试建立数据库连接。

image.png

我用个生活中的比喻解释一下:

想象你奶奶想打个电话,但是家里没有电话机。为了能打电话,首先需要去买一个电话机,把它正确安装好并插到电话线上。这相当于加载MySQL的驱动程序类。

家里买来的电话机不会自动连到电话线上,必须手动把它安装插好才能用。同样地,刚加载的MySQL驱动也不会自动可用,必须手动将它"注册"到管理所有电话机的系统中。

这个"注册"的过程,就好比你需要拨一个特殊的号码,把新买的电话机报给电话公司,告诉他们:"嘿,我家新装了一个电话机,请把它加入你们的系统里"。

一旦电话公司的系统里有了你家的新电话机,将来就可以用它打电话了。同理,注册后的MySQL驱动也可以被Java程序使用,连接数据库了。

所以那行Class.forName代码,相当于你先买回家一个电话机(加载驱动);而在它的静态代码块中,则自动帮你打了个内线电话给电话公司(注册驱动),"嘿,我是新的MySQL电话机,请记录我"。

3.2Connection

  • Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。

  • 在建立连接时,需要指定数据库URL、用户名、密码参数。

    • URL:jdbc:mysql://localhost:3306/atguigu
    • jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2
  • Connection 接口还负责管理事务,Connection 接口提供了 commitrollback 方法,用于提交事务和回滚事务。

  • 可以创建 Statement 对象,用于执行 SQL 语句并与数据库进行交互。

  • 在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。

3.3Statement

  • Statement 接口用于执行 SQL 语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过 Statement 对象,可以向数据库发送 SQL 语句并获取执行结果。

  • 结果可以是一个或多个结果。

    • 增删改:受影响行数单个结果。
    • 查询:单行单列、多行多列、单行多列等结果。
  • 但是Statement 接口在执行SQL语句时,会产生SQL注入攻击问题:

    • 当使用 Statement 执行动态构建的 SQL 查询时,往往需要将查询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。

示例代码:

String username = "' OR '1'='1"; // 用户输入的非法用户名
String sql = "SELECT * FROM users WHERE username='" + username + "'";

Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 结果集将包含整个 users 表的所有记录

在这段代码中,我们动态构建了一个 SQL 查询语句,并将用户输入的username直接拼接到 SQL 语句中。但是,用户输入的用户名是一个非法的字符串 ' OR '1'='1

当执行 SQL 语句时,实际执行的是:

SELECT * FROM users WHERE username='' OR '1'='1'

由于'1'='1'这个条件永远为真,因此该查询会返回 users 表中的所有记录,而不是预期的基于用户名进行过滤的结果。

这种注入攻击发生的根本原因是,用户的输入数据被直接拼接到 SQL 语句中,而没有经过任何过滤和检查。攻击者可以构造特殊的字符串,影响原本的 SQL 语句执行逻辑,从而绕过认证或获取不应有的数据。

这种 SQL 注入攻击不仅可能导致数据泄露,还可能被利用执行任意的 SQL 语句,如删除或修改数据库数据等,造成严重的安全隐患。

因此,在使用 JDBC 时,应当避免使用Statement执行动态构建的 SQL 语句,而是使用PreparedStatementPreparedStatement允许使用参数占位符,将参数数据与 SQL 语句分离,有效防止 SQL 注入攻击的发生。

dubug演示: image.png

代码:

package com.rainsoul.base;  
  
import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.ResultSet;  
import java.sql.Statement;  
import java.util.Scanner;  
  
public class JDBCInjection {  
    public static void main(String[] args) throws Exception {  
        //1.注册驱动 (可以省略)  
  
        //2.获取连接对象  
        Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
        //3.获取执行SQL语句对象  
        Statement statement = connection.createStatement();  
  
        System.out.println("请输入员工姓名:");  
        Scanner scanner = new Scanner(System.in);  
        String name = scanner.nextLine();  
  
        //4.编写SQL语句,并执行,接受返回的结果  
        String sql = "SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = '"+name+"'";  
        ResultSet resultSet = statement.executeQuery(sql);  
  
        //5.处理结果:遍历resultSet  
        while(resultSet.next()){  
            int empId = resultSet.getInt("emp_id");  
            String empName = resultSet.getString("emp_name");  
            double empSalary = resultSet.getDouble("emp_salary");  
            int empAge = resultSet.getInt("emp_age");  
            System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);  
        }  
  
        //6.释放资源  
        resultSet.close();  
        statement.close();  
        connection.close();  
    }  
}

3.4 PreparedStatement

  • PreparedStatementStatement 接口的子接口,用于执行预编译的 SQL 查询,作用如下:
    1. 预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。
    2. 防止SQL注入:PreparedStatement 支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来'',无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。
    3. 性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。

image.png

代码:

package com.rainsoul.base;  
  
import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.PreparedStatement;  
import java.sql.ResultSet;  
import java.util.Scanner;  
  
public class JDBCPrepared {  
    public static void main(String[] args) throws Exception{  
        //1.注册驱动 (可以省略)  
  
        //2.获取连接对象  
        Connection connection = DriverManager  
                .getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
        //3.获取执行SQL语句对象  
        PreparedStatement preparedStatement = connection  
                .prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = ?");  
  
        System.out.println("请输入员工姓名:");  
        Scanner scanner = new Scanner(System.in);  
        String name = scanner.nextLine();  
  
        //4.为?占位符复制,并执行SQL语句,接受返回的结果  
        preparedStatement.setString(1, name);  
        ResultSet resultSet = preparedStatement.executeQuery();  
  
        //5.处理结果:遍历resultSet  
        while(resultSet.next()){  
            int empId = resultSet.getInt("emp_id");  
            String empName = resultSet.getString("emp_name");  
            double empSalary = resultSet.getDouble("emp_salary");  
            int empAge = resultSet.getInt("emp_age");  
            System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);  
        }  
  
        //6.释放资源  
        resultSet.close();  
        preparedStatement.close();  
        connection.close();  
    }  
}

3.5ResultSet

  • ResultSet是 JDBC API 中的一个接口,用于表示从数据库中执行查询语句所返回的结果集。它提供了一种用于遍历和访问查询结果的方式。
  • 遍历结果:ResultSet可以使用 next() 方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。
  • 获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。

4. 基于PreparedStatement实现CRUD

4.1查询单行单列

@Test  
public void testQuerySingleRowAndCol() throws SQLException {  
    //1.注册驱动 (可以省略)  
  
    //2.获取连接  
    Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    //3.预编译SQL语句得到PreparedStatement对象  
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(*) as count FROM t_emp");  
  
    //4.执行SQL语句,获取结果  
    ResultSet resultSet = preparedStatement.executeQuery();  
  
    //5.处理结果(如果自己明确一定只有一个结果,那么resultSet最少要做一次next的判断,才能拿到我们要的列的结果)  
    if(resultSet.next()){  
        int count = resultSet.getInt("count");  
        System.out.println(count);  
    }  
  
    //6.释放资源  
    resultSet.close();  
    preparedStatement.close();  
    connection.close();  
}

4.2查询单行多列

@Test  
public void testQuerySingleRow()throws Exception{  
    //1.注册驱动  
  
    //2.获取连接  
    Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    //3.预编译SQL语句获得PreparedStatement对象  
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_id = ?");  
  
    //4.为占位符赋值,然后执行,并接受结果  
    preparedStatement.setInt(1,5);  
    ResultSet resultSet = preparedStatement.executeQuery();  
  
    //5.处理结果  
    while(resultSet.next()){  
        int empId = resultSet.getInt("emp_id");  
        String empName = resultSet.getString("emp_name");  
        double empSalary = resultSet.getDouble("emp_salary");  
        int empAge = resultSet.getInt("emp_age");  
  
        System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);  
    }  
  
    //6.资源释放  
    resultSet.close();  
    preparedStatement.close();  
    connection.close();  
}

4.3查询多行多列

@Test  
public void testQueryMoreRow()throws Exception{  
    Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_age > ?");  
  
    //为占位符赋值,执行SQL语句,接受结果  
    preparedStatement.setInt(1, 25);  
    ResultSet resultSet = preparedStatement.executeQuery();  
  
    while(resultSet.next()){  
        int empId = resultSet.getInt("emp_id");  
        String empName = resultSet.getString("emp_name");  
        double empSalary = resultSet.getDouble("emp_salary");  
        int empAge = resultSet.getInt("emp_age");  
  
        System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);  
    }  
  
    resultSet.close();  
    preparedStatement.close();  
    connection.close();  
}

4.4新增

@Test  
public void testInsert() throws SQLException {  
    Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)");  
  
    preparedStatement.setString(1, "rose");  
    preparedStatement.setDouble(2,345.67);  
    preparedStatement.setInt(3,28);  
  
    int result = preparedStatement.executeUpdate();  
  
    //根据受影响行数,做判断,得到成功或失败  
    if(result > 0){  
        System.out.println("成功!");  
    }else{  
        System.out.println("失败!");  
    }  
  
    preparedStatement.close();  
    connection.close();  
  
}

4.5修改

@Test  
public void testUpdate() throws SQLException {  
    Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    PreparedStatement preparedStatement = connection.prepareStatement("UPDATE t_emp SET emp_salary = ? WHERE emp_id = ?");  
  
    preparedStatement.setDouble(1, 888.88);  
    preparedStatement.setInt(2, 6);  
  
    int result = preparedStatement.executeUpdate();  
  
    if(result > 0){  
        System.out.println("成功!");  
    }else{  
        System.out.println("失败!");  
    }  
  
    preparedStatement.close();  
    connection.close();  
}

4.6删除

@Test  
public void testDelete() throws SQLException {  
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");  
  
    PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM t_emp WHERE emp_id = ?");  
  
    preparedStatement.setDouble(1, 6);  
  
    int result = preparedStatement.executeUpdate();  
  
    if(result > 0){  
        System.out.println("成功!");  
    }else{  
        System.out.println("失败!");  
    }  
  
    preparedStatement.close();  
    connection.close();  
}

5.JDBC扩展

5.1实体类和ORM

  • 在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!
  • ORM(Object Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
  • 当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习ORM框架,比如MyBatis、JPA等。

新建POJO类: image.png

//类名和数据库名对应,但是表名一般缩写,类名要全写!
public class Employee {
    private Integer empId;//emp_id = empId 数据库中列名用下划线分隔,属性名用驼峰!
    private String empName;//emp_name = empName
    private Double empSalary;//emp_salary = empSalary
    private Integer empAge;//emp_age = empAge

    get set...
}

封装代码:

@Test  
public void testORM() throws SQLException {  
    // 获取数据库连接  
    Connection connection = DriverManager  
            .getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    // 准备执行SQL语句的预编译语句,用于提高查询效率和防止SQL注入  
    PreparedStatement preparedStatement = connection  
            .prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_id = ?");  
  
    // 设置预编译语句中的参数  
    preparedStatement.setInt(1,1);  
  
    // 执行查询并获取结果集  
    ResultSet resultSet = preparedStatement.executeQuery();  
  
    // 初始化Employee对象,用于存放查询结果  
    Employee employee = null;  
  
    // 遍历结果集并将数据赋值给Employee对象  
    if(resultSet.next()){  
        employee = new Employee();  
        int empId = resultSet.getInt("emp_id");  
        String empName = resultSet.getString("emp_name");  
        double empSalary = resultSet.getDouble("emp_salary");  
        int empAge = resultSet.getInt("emp_age");  
        // 将查询结果映射到Employee对象的属性上  
        employee.setEmpId(empId);  
        employee.setEmpName(empName);  
        employee.setEmpSalary(empSalary);  
        employee.setEmpAge(empAge);  
    }  
  
    // 打印Employee对象,验证查询和映射结果  
    System.out.println(employee);  
  
    // 关闭结果集、预编译语句和数据库连接,释放资源  
    resultSet.close();  
    preparedStatement.close();  
    connection.close();  
}

5.2主键回显

在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。

@Test  
public void testReturnPK()throws Exception{  
    //获取连接  
    Connection connection = DriverManager  
            .getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    // 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值  
    String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";  
    PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);  
  
    // 创建对象,将对象的属性值,填充在?占位符上 (ORM)  
    Employee employee = new Employee(null, "jack", 123.45, 29);  
    preparedStatement.setString(1, employee.getEmpName());  
    preparedStatement.setDouble(2, employee.getEmpSalary());  
    preparedStatement.setInt(3, employee.getEmpAge());  
  
    //执行SQL,并获取返回的结果  
    int result = preparedStatement.executeUpdate();  
    ResultSet resultSet = null;  
    //处理结果  
    if(result > 0){  
        System.out.println("成功!");  
  
        //获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。  
        //返回的主键值,是一个单行单列的结果存储在ResultSet里  
        resultSet = preparedStatement.getGeneratedKeys();  
        if(resultSet.next()){  
            int empId = resultSet.getInt(1);  
            employee.setEmpId(empId);  
        }  
  
        System.out.println(employee);  
    }else{  
        System.out.println("失败!");  
    }  
  
    //释放资源  
    if(resultSet!=null){  
        resultSet.close();  
    }  
    preparedStatement.close();  
    connection.close();  
}

5.3批量操作

一条一条操作很耗时间:

@Test  
public void testReturnPK()throws Exception{  
    //获取连接  
    Connection connection = DriverManager  
            .getConnection("jdbc:mysql:///rainsoul", "root", "root");  
  
    // 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值  
    String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";  
    PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);  
  
    // 创建对象,将对象的属性值,填充在?占位符上 (ORM)  
    Employee employee = new Employee(null, "jack", 123.45, 29);  
    preparedStatement.setString(1, employee.getEmpName());  
    preparedStatement.setDouble(2, employee.getEmpSalary());  
    preparedStatement.setInt(3, employee.getEmpAge());  
  
    //执行SQL,并获取返回的结果  
    int result = preparedStatement.executeUpdate();  
    ResultSet resultSet = null;  
    //处理结果  
    if(result > 0){  
        System.out.println("成功!");  
  
        //获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。  
        //返回的主键值,是一个单行单列的结果存储在ResultSet里  
        resultSet = preparedStatement.getGeneratedKeys();  
        if(resultSet.next()){  
            int empId = resultSet.getInt(1);  
            employee.setEmpId(empId);  
        }  
  
        System.out.println(employee);  
    }else{  
        System.out.println("失败!");  
    }  
  
    //释放资源  
    if(resultSet!=null){  
        resultSet.close();  
    }  
    preparedStatement.close();  
    connection.close();  
}

优化后:

@Test  
    public void testBatch() throws Exception {  
        //1.注册驱动  
//        Class.forName("com.mysql.cj.jdbc.Driver");  
  
        //2.获取连接  
        Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul?rewriteBatchedStatements=true", "root", "root");  
  
        //3.编写SQL语句  
        /*  
            注意:1、必须在连接数据库的URL后面追加?rewriteBatchedStatements=true,允许批量操作  
                2、新增SQL必须用values。且语句最后不要追加;结束  
                3、调用addBatch()方法,将SQL语句进行批量添加操作  
                4、统一执行批量操作,调用executeBatch()  
         */        String sql = "insert into t_emp (emp_name,emp_salary,emp_age) values (?,?,?)";  
  
        //4.创建预编译的PreparedStatement,传入SQL语句  
        PreparedStatement preparedStatement = connection.prepareStatement(sql);  
  
        //获取当前行代码执行的时间。毫秒值  
        long start = System.currentTimeMillis();  
        for(int i = 0;i<10000;i++){  
            //5.为占位符赋值  
            preparedStatement.setString(1, "marry"+i);  
            preparedStatement.setDouble(2, 100.0+i);  
            preparedStatement.setInt(3, 20+i);  
  
            preparedStatement.addBatch();  
        }  
  
        //执行批量操作  
        preparedStatement.executeBatch();  
  
        long end = System.currentTimeMillis();  
  
        System.out.println("消耗时间:"+(end - start));  
  
        preparedStatement.close();  
        connection.close();  
    }

6.连接池

我们每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁造成资源浪费。连接的数量无法把控,对服务器来说压力巨大。

连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。

预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。

当池中无连接可用,且未达到上限时,连接池会新建连接。

池中连接达到上限,用户请求会等待,可以设置超时时间。

好的,下面是常见的几种连接池及其简要讲解:

连接池 描述 Apache DBCP Apache 开发的数据库连接池,实现了连接池的基本管理功能,并对获取连接、使用连接、释放连接等过程进行了简单的封装,是一个老牌、经典的连接池。 C3P0 一个开源的JDBC连接池产品,实现了数据源和连接池的功能。它支持JDBC3规范和JDBC2的扩展,包括连接分组、强制断线重连、统计和扩展语句级别等功能。 Tomcat JDBC Connection Pool Tomcat服务器自带的数据库连接池,可以直接使用或集成到其他服务器环境中。相比Apache DBCP和C3P0,它配置更简单,且内置于Tomcat中,占用资源较少。 HikariCP 一个轻量级的高性能连接池,它是为了解决Apache DBCP、C3P0等连接池存在的并发性能问题和内存泄漏问题而设计的。相比其他连接池,HikariCP更快、更稳定,资源利用率更高。 Druid Alibaba开源的一个数据库连接池项目,它包含一个高效的连接池和监控组件。Druid能够提供强大的监控和扩展功能,帮助开发人员快速发现系统中存在的问题。 BoneCP 一个极简连接池实现,它在性能和低内存占用方面表现良好。BoneCP设计为轻量级并且容易使用,但它缺乏其他连接池提供的一些高级功能。

选择合适的连接池要考虑项目的实际需求、性能要求、可维护性等因素。一般来说,HikariCP和Druid因为性能和功能方面的出色表现,使用较为广泛。Apache DBCP和C3P0作为传统的连接池也有一定使用。不同场景下,可以根据具体情况选择适合的连接池产品。

6.1Druid连接池使用

首先要引入jar包。

image.png

/**  
 * 测试通过硬编码方式使用Druid连接池。  
 * 该方法展示了如何直接在代码中配置Druid连接池,并使用该连接池获取数据库连接,  
 * 进行操作后,再将连接归还给连接池。  
 *   
* @throws SQLException 如果操作数据库连接时发生错误,则抛出SQLException。  
 */  
@Test  
public void testHardCodeDruid() throws SQLException {  
    /*  
        硬编码:将连接池的配置信息和Java代码耦合在一起。  
        1、创建DruidDataSource连接池对象。  
        2、设置连接池的配置信息【必须 | 非必须】  
        3、通过连接池获取连接对象  
        4、回收连接【不是释放连接,而是将连接归还给连接池,给其他线程进行复用】  
     */  
    //1.创建DruidDataSource连接池对象。  
    DruidDataSource druidDataSource = new DruidDataSource();  
  
    //2.设置连接池的配置信息【必须 | 非必须】  
    //2.1 必须设置的配置  
    druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");  
    druidDataSource.setUrl("jdbc:mysql:///rainsoul");  
    druidDataSource.setUsername("root");  
    druidDataSource.setPassword("root");  
  
    //2.2 非必须设置的配置,例如初始化大小和最大活动连接数  
    druidDataSource.setInitialSize(10);  
    druidDataSource.setMaxActive(20);  
      
    //3.通过连接池获取连接对象  
    Connection connection = druidDataSource.getConnection();  
    System.out.println(connection);  
  
    //基于connection进行数据库操作,例如CRUD  
  
    //4.回收连接,将使用完毕的连接归还给连接池  
    connection.close();  
}

使用配置文件: image.png

/**  
 * 测试通过Druid连接池获取数据库连接的功能  
 * 本测试方法不接受参数,也不返回任何值  
 * @throws Exception 抛出异常的条件:读取配置文件或获取数据库连接时可能发生的任何异常  
 */  
@Test  
public void testResourcesDruid() throws Exception {  
    // 创建Properties对象用于存放配置信息  
    Properties properties = new Properties();  
  
    // 从类路径下读取db.properties配置文件,并加载到Properties对象中  
    InputStream inputStream = DruidTest.class.getClassLoader()  
            .getResourceAsStream("db.properties");  
    properties.load(inputStream);  
  
    // 使用DruidDataSourceFactory和配置信息创建DruidDataSource连接池  
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);  
  
    // 从连接池中获取一个数据库连接  
    Connection connection = dataSource.getConnection();  
    System.out.println(connection);  
  
    // 此处是开发CRUD操作的代码位置,当前代码未实现具体操作  
  
    // 使用完毕后关闭数据库连接,释放资源  
    connection.close();  
}

6.2Hikari连接池的使用

@Test  
public void testHardCodeHikari()throws Exception{  
    /*  
        硬编码:将连接池的配置信息和Java代码耦合在一起  
        1、创建HikariDataSource连接池对象  
        2、设置连接池的配置信息【必须 | 非必须】  
        3、通过连接池获取连接对象  
        4、回收连接  
     */    //1.创建HikariDataSource连接池对象  
    HikariDataSource hikariDataSource = new HikariDataSource();  
  
    //2.设置连接池的配置信息【必须 | 非必须】  
    //2.1 必须设置的配置  
    hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");  
    hikariDataSource.setJdbcUrl("jdbc:mysql:///rainsoul");  
    hikariDataSource.setUsername("root");  
    hikariDataSource.setPassword("root");  
  
    //2.2 非必须设置的配置  
    hikariDataSource.setMinimumIdle(10);  
    hikariDataSource.setMaximumPoolSize(20);  
  
    //3.通过连接池获取连接对象  
    Connection connection = hikariDataSource.getConnection();  
    System.out.println(connection);  
  
    //4.回收连接  
    connection.close();  
}

使用配置文件: image.png

@Test  
public void testResourcesHikari() throws Exception {  
    //1.创建Properties集合,用于存储外部配置文件的key和value值。  
    Properties properties = new Properties();  
  
    //2.读取外部配置文件,获取输入流,加载到Properties集合里。  
    InputStream inputStream = HikariTest.class.getClassLoader()  
            .getResourceAsStream("hikari.properties");  
    properties.load(inputStream);  
  
    //3.创建HikariConfig连接池配置对象,将Properties集合传进去。  
    HikariConfig hikariConfig = new HikariConfig(properties);  
  
    //4.基于HikariConfig连接池配置对象,构建HikariDataSource  
    HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);  
  
    //5.获取连接  
    Connection connection = hikariDataSource.getConnection();  
    System.out.println(connection);  
  
    //6.回收连接  
    connection.close();  
}

6.3常见的参数配置

参数名称 描述 initialSize 连接池初始连接数量,默认值通常为0 maxActive 连接池在同一时间能够分配的最大活跃连接数,使用负值表示不限制 maxIdle 连接池中最大空闲连接数,控制池中有多少空闲连接可以存活 minIdle 连接池中最小空闲连接数,低于这个数量时,连接池会创建新的连接 maxWait 当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计),超过时间则抛出异常 maxAge 连接池中连接能够存活的最长时间(以毫秒计),超过时间将被释放 testOnBorrow 在将连接借出时是否测试连接的有效性,可避免将无效连接分配出去 testOnReturn 在将连接归还到池中时是否测试连接的有效性,可避免将无效连接存入池中 testWhileIdle 是否对空闲连接进行有效性检测,可避免连接由于长期空闲而失效 timeBetweenEvictionRunsMillis 空闲连接检测线程的运行周期时间,用于控制空闲连接检测的频率 validationQuery 用于检测连接是否有效的SQL查询语句,如 "SELECT 1" removeAbandonedTimeout 连接长时间无操作时,被视为已经被废弃的超时时间(以秒计) logAbandoned 是否记录长时间无操作而被废弃的连接信息

根据不同的连接池实现,可配置参数的名称可能有所不同,但基本含义是相似的。正确配置这些参数可以合理控制连接池的大小、连接的生命周期、资源利用率等,从而优化连接池的性能和资源占用。

7.JDBC优化及工具类封装

7.1JDBC工具类(V1.0):

  1. 维护一个连接池对象。
  2. 对外提供在连接池中获取连接的方法
  3. 对外提供回收连接的方法 注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法!

这段代码实现了一个JDBC工具类 JDBCUtil,用于管理数据库连接池和获取/释放连接。我将为每部分代码添加注释进行解释。

public class JDBCUtil {
    // 创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的。
    private static DataSource dataSource;

    // 在项目启动时,即创建连接池对象,赋值给dataSource
    static {
        try {
            // 创建一个Properties对象,用于加载配置文件
            Properties properties = new Properties();
            // 获取配置文件的输入流
            InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
            // 加载配置文件
            properties.load(inputStream);

            // 使用Druid连接池工厂类创建连接池对象,并传入配置文件参数
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            // 如果创建连接池出现异常,则抛出运行时异常
            throw new RuntimeException(e);
        }
    }

    // 对外提供在连接池中获取连接的方法
    public static Connection getConnection() {
        try {
            // 从连接池中获取一个连接
            return dataSource.getConnection();
        } catch (SQLException e) {
            // 如果获取连接出现异常,则抛出运行时异常
            throw new RuntimeException(e);
        }
    }

    // 对外提供回收连接的方法
    public static void release(Connection connection) {
        try {
            // 关闭连接,将其归还到连接池中
            connection.close();
        } catch (SQLException e) {
            // 如果关闭连接出现异常,则抛出运行时异常
            throw new RuntimeException(e);
        }
    }
}

这个工具类主要实现了以下功能:

  1. 在静态代码块中,通过读取配置文件 db.properties 获取数据库连接参数,并使用 Druid 连接池工厂类创建一个连接池对象 dataSource
  2. 提供一个静态方法 getConnection(),用于从连接池中获取一个数据库连接。
  3. 提供一个静态方法 release(Connection connection),用于关闭指定的数据库连接,将其归还到连接池中。

使用这个工具类,可以方便地获取和释放数据库连接,同时利用连接池技术提高连接的复用率和性能。需要注意的是,在项目启动时就创建了连接池对象,并在静态代码块中加载了配置文件,这种方式确保了连接池的初始化只执行一次。

7.2ThreadLocal

用一个生活中的例子来解释什么是ThreadLocal以及它的使用场景。

我们可以把ThreadLocal想象成一个小柜子,每个人都有自己专属的小柜子。这个小柜子就相当于每个线程内部的存储空间。

假设你奶奶有几个孙子孙女,他们都很顽皮,经常在家里到处乱跑。为了防止他们弄乱房间,你给每个孩子准备了一个专属的小柜子,让他们把自己的东西放进去。

这样一来,每个孩子拿自己柜子里的东西就不会影响其他孩子,也不会把房间弄乱了。这就是ThreadLocal的作用,它为每个"线程"提供了一个独立的存储空间,可以在里面放自己的东西,而不会影响到其他"线程"。

在程序中,多个线程就像你奶奶家的这些孩子一样,他们并发地运行,有可能会争夺一些共享的资源(比如数据库连接对象)。如果多个线程使用同一个资源,就可能会导致线程安全问题。

通过ThreadLocal,每个线程就拥有了自己独立的"小柜子",可以放置自己的连接对象。当线程需要使用连接时,从自己的"小柜子"里取出来用;用完后,又放回到"小柜子"中,不会影响其他线程。

这种做法避免了线程之间相互影响和频繁地从连接池中获取连接,提高了程序的运行效率和线程安全性。

当然,我们要记得在线程结束后,将"小柜子"里的东西清理干净,避免浪费资源。否则就像孩子长大后忘记把自己的旧东西清理出柜子一样,会导致资源的浪费和积累。

总之,ThreadLocal就像是为每个线程准备了一个"独立的小空间",保证了线程之间使用共享资源时的隔离性和安全性。只要用好了,就可以让程序运行得更高效和可靠。

ThreadLocal的相关知识点整理如下:

  1. ThreadLocal概念

    • ThreadLocal是JDK包中的一个类,可以在同一个线程内创建独立的副本变量。
    • 每个线程都会有一个自己独立的副本变量,线程之间彼此不会相互影响。
    • ThreadLocal提供get()、set(value)、remove()等方法访问和修改副本变量。
  2. ThreadLocal原理

    • 每个Thread内部都有一个ThreadLocalMap类型的成员变量,存储当前线程的副本变量。
    • ThreadLocal作为Map的键(key),副本变量作为Map的值(value)存储。
    • 每个线程读写自己所属线程的变量副本,线程之间相互隔离。
  3. ThreadLocal使用场景

    • 线程本地存储(每个线程内部有自己独立的变量副本)
    • 事务管理(绑定事务上下文)
    • 日志记录(MDC机制存储日志跟踪信息)
    • 数据库连接池(每个线程有独立的连接对象)
  4. ThreadLocal使用注意事项

    • 每个线程自己使用后,需要调用remove()清除副本,避免内存泄漏。
    • 副本变量是存储在线程对象内部的Map中,生存周期随线程终止而销毁。
    • 减小副本变量的范围,使用后再强制设为null,避免副本变量带来的额外内存占用。
    • 只有在状态确实需要与线程相关联时,才使用ThreadLocal存储状态。
  5. ThreadLocal应用实例

    • JDBC中使用ThreadLocal获取线程独立的Connection对象
    • Servlet组件绑定和传递Request、Response等对象
    • 实现简化的线程上下文管理器(TransmittableThreadLocal)
  6. ThreadLocal内存泄漏风险

    • 由于ThreadLocalMap的生存周期是与线程一直存在的,若不手动remove,就会导致内存泄漏。
    • 使用弱引用或定期删除无效线程变量,防止内存泄漏。
    • 在线程池中注意每次运行结束后,清理ThreadLocal存储。

JDBCUtilV2工具类(V2.0)

这个 JDBCUtilV2 类是在原有 JDBCUtil 类的基础上进行了改进,增加了 ThreadLocal 的支持。我将为这部分新增的代码添加注释进行解释。

public class JDBCUtilV2 {
    // ...

    // 创建一个ThreadLocal对象,用于存储线程级别的Connection对象
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    // ...

    // 对外提供在连接池中获取连接的方法
    public static Connection getConnection() {
        try {
            // 从ThreadLocal中获取当前线程绑定的Connection对象
            Connection connection = threadLocal.get();
            // 如果ThreadLocal中没有绑定Connection对象
            if (connection == null) {
                // 从连接池中获取一个新的连接
                connection = dataSource.getConnection();
                // 将新的连接对象绑定到当前线程的ThreadLocal中
                threadLocal.set(connection);
            }
            return connection;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    // 对外提供回收连接的方法
    public static void release() {
        try {
            // 从ThreadLocal中获取当前线程绑定的Connection对象
            Connection connection = threadLocal.get();
            if (connection != null) {
                // 从当前线程的ThreadLocal中移除绑定的Connection对象
                threadLocal.remove();
                // 如果开启了事务的手动提交,操作完毕后,归还给连接池之前,要将事务的自动提交改为true
                connection.setAutoCommit(true);
                // 将连接对象归还给连接池
                connection.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

这个改进的版本利用了 ThreadLocal 这个线程内部的数据存储类,实现了每个线程拥有自己独立的 Connection 对象。具体来说:

  1. 声明了一个静态的 ThreadLocal<Connection> 对象 threadLocal
  2. getConnection() 方法中,首先尝试从当前线程的 ThreadLocal 中获取 Connection 对象。如果获取不到,则从连接池中获取一个新的连接,并将其绑定到当前线程的 ThreadLocal 中。
  3. release() 方法中,从当前线程的 ThreadLocal 中获取绑定的 Connection 对象。如果存在,则先将其从 ThreadLocal 中移除,然后将连接的自动提交设置为 true(如果之前手动设置过),最后将连接归还给连接池。

使用 ThreadLocal 的好处是每个线程都可以独立获取和使用自己的 Connection 对象,避免了多线程环境下的线程安全问题。同时,在操作完成后,通过调用 release() 方法,可以将 Connection 对象正确地归还给连接池,实现了连接的复用。

需要注意的是,在使用 ThreadLocal 时,必须确保在线程结束后正确地释放资源,否则可能会导致内存泄漏。在这个例子中,由于 ThreadLocal 对象是静态的,所以需要通过其他方式(如在Web应用中监听ServletContextListener事件)来清理 ThreadLocal

image.png

拿到同一个。

8.DAO封装及BaseDAO工具类

DAO模式的几个核心点:

  1. 职责划分:DAO层专注于对数据库的访问操作,不涉及业务逻辑,将数据库操作与业务逻辑解耦。业务逻辑由Service层负责处理。

  2. 面向对象:Java天生面向对象,一个DAO对象对应一张数据库表,每个DAO对象维护这张表的CRUD(增删改查)操作方法。

  3. 封装性:DAO对象对外提供标准化的数据访问API接口,上层模块无需关心数据库访问的具体实现细节。

  4. 易于维护:数据库操作均集中在DAO层,如果需要更换底层数据存储方式,只需修改DAO层即可,上层代码无需改动。

  5. 复用性:通用的数据访问方法可以在DAO层得到复用,提高代码的重用率。

  6. 规范性:引入DAO层有利于遵循设计模式规范,促进项目结构清晰和可维护性。

基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,复用增删改查的基本操作,我们称为BaseDAO。

8.1创建员工DAO接口

/**  
 * EmployeeDao这个类对应的是t_emp这张表的增删改查的操作  
 */  
public interface EmployeeDao {  
    /**  
     * 数据库对应的查询所有的操作  
     * @return 表中所有的数据  
     */  
    List<Employee> selectAll();  
  
    /**  
     * 数据库对应的根据empId查询单个员工数据操作  
     * @param empId 主键列  
     * @return 一个员工对象(一行数据)  
     */  
    Employee selectByEmpId(Integer empId);  
  
    /**  
     * 数据库对应的新增一条员工数据  
     * @param employee ORM思想中的一个员工对象  
     * @return 受影响的行数  
     */  
    int insert(Employee employee);  
  
    /**  
     * 数据库对应的修改一条员工数据  
     * @param employee ORM思想中的一个员工对象  
     * @return 受影响的行数  
     */  
    int update(Employee employee);  
  
    /**  
     * 数据库对应的根据empId删除一条员工数据  
     * @param empId 主键列  
     * @return 受影响的行数  
     */  
    int delete(Integer empId);  
}
public class EmployeeDaoImpl extends BaseDAO implements EmployeeDao {  
    @Override  
    public List<Employee> selectAll() {  
        try {  
            String sql = "SELECT emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge FROM t_emp";  
            return executeQuery(Employee.class,sql,null);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public Employee selectByEmpId(Integer empId) {  
        try {  
            String sql = "SELECT emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge FROM t_emp where emp_id = ?";  
            return executeQueryBean(Employee.class,sql,empId);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public int insert(Employee employee) {  
        try {  
            String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";  
            return executeUpdate(sql,employee.getEmpName(),employee.getEmpSalary(),employee.getEmpAge());  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public int update(Employee employee) {  
        try {  
            String sql = "UPDATE t_emp SET emp_salary = ? WHERE emp_id = ?";  
            return executeUpdate(sql,employee.getEmpSalary(),employee.getEmpId());  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public int delete(Integer empId) {  
        try {  
            String sql = "delete from t_emp where emp_id = ?";  
            return executeUpdate(sql,empId);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
}

8.2BaseDAO搭建

/**  
 * 将共性的数据库的操作代码封装在BaseDAO里。  
 */  
public class BaseDAO {  
    /**  
     * 通用的增删改的方法。  
     * @param sql 调用者要执行的SQL语句  
     * @param params SQL语句中的占位符要赋值的参数  
     * @return 受影响的行数  
     */  
    public int executeUpdate(String sql,Object... params)throws Exception{  
        //1.通过JDBCUtilV2获取数据库连接  
        Connection connection = JDBCUtilV2.getConnection();  
  
        //2.预编译SQL语句  
        PreparedStatement preparedStatement = connection.prepareStatement(sql);  
  
        //4.为占位符赋值,执行SQL,接受返回结果  
        if(params!=null && params.length > 0){  
            for (int i = 0; i < params.length; i++) {  
                //占位符是从1开始的。参数的数组是从0开始的  
                preparedStatement.setObject(i+1,params[i] );  
            }  
        }  
        int row = preparedStatement.executeUpdate();  
  
        //5.释放资源  
        preparedStatement.close();  
        if(connection.getAutoCommit()){  
            JDBCUtilV2.release();  
        }  
  
        //6.返回结果  
        return row;  
    }  
  
    /**  
     * 通用的查询:多行多列、单行多列、单行单列  
     *      多行多列:List<Employee>  
     *      单行多列:Employee  
     *      单行单列:封装的是一个结果。Double、Integer、。。。。。  
     *  封装过程:  
     *      1、返回的类型:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了。  
     *      2、返回的结果:通用,List  可以存储多个结果,也可以存储一个结果 get(0)  
     *      3、结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class  
     */    public <T> List<T> executeQuery(Class<T> clazz,String sql,Object... params)throws Exception{  
        //获取连接  
        Connection connection = JDBCUtilV2.getConnection();  
  
        //预编译SQL语句  
        PreparedStatement preparedStatement = connection.prepareStatement(sql);  
  
        //设置占位符的值  
        if(params!=null && params.length > 0){  
            for (int i = 0; i < params.length; i++) {  
                preparedStatement.setObject(i+1, params[i]);  
            }  
        }  
        //执行SQL,并接受返回的结果集  
        ResultSet resultSet = preparedStatement.executeQuery();  
  
        //获取结果集中的元数据对象  
        //包含了:列的数量、每个列的名称  
        ResultSetMetaData metaData = resultSet.getMetaData();  
        int columnCount = metaData.getColumnCount();  
  
        List<T> list = new ArrayList<>();  
        //处理结果  
        while(resultSet.next()){  
            //循环一次,代表有一行数据,通过反射创建一个对象  
            T t = clazz.newInstance();  
            //循环遍历当前行的列,循环几次,看有多少列  
            for (int i = 1; i <=columnCount ;i++){  
                //通过下表获取列的值  
                Object value = resultSet.getObject(i);  
  
                //获取到的列的value值,这个值就是t这个对象中的某一个属性  
                //获取当前拿到的列的名字 = 对象的属性名  
                String fieldName = metaData.getColumnLabel(i);  
                //通过类对象和fieldName获取要封装的对象的属性  
                Field field = clazz.getDeclaredField(fieldName);  
                //突破封装的private  
                field.setAccessible(true);  
                field.set(t,value);  
            }  
            list.add(t);  
        }  
        resultSet.close();  
        preparedStatement.close();  
        if(connection.getAutoCommit()){  
            JDBCUtilV2.release();  
        }  
  
        return list;  
    }  
  
    /**  
     * 通用查询:在上面查询的集合结果中获取第一个结果。 简化了获取单行单列的获取、单行多列的获取  
     */  
    public <T> T executeQueryBean(Class<T> clazz,String sql,Object... params)throws Exception{  
        List<T> list = this.executeQuery(clazz, sql, params);  
        if(list ==null || list.size() == 0){  
            return null;  
        }  
       return list.get(0);  
    }  
}

9.事务

9.1事务(Transaction)的概念:

事务是逻辑上的一组操作,要么全部执行,要么全不执行。它是数据库运行中的逻辑工作单位,由一个有限的数据库操作序列构成。

事务的特性(ACID):

  1. 原子性(Atomicity): 事务作为一个整体,不可分割。事务的所有操作要么全部成功,要么全部失败回滚。
  2. 一致性(Consistency): 事务执行前后,数据库都保持一致状态。所有约束都应该被保存。
  3. 隔离性(Isolation): 事务之间是相互隔离的,彼此不会相互影响。每个事务只能看到其他并行事务提交之前的数据。
  4. 持久性(Durability): 一旦事务提交成功,对数据的改变就是永久的,即使出现系统failure,也不会丢失。

事务的状态:

  1. 活跃(Active): 事务正在执行中的状态,更新会被暂存。
  2. 延迟(Partially Committed): 事务执行的最后一个语句时的状态。
  3. 提交(Committed): 事务中所有更新都已经写入数据库,不可逆。
  4. 回滚(Rollback): 撤销事务中所有更新操作,回到事务开始前的状态。
  5. 失败(Failed): 由于某种原因导致事务无法正常执行,如系统crash等。

事务的控制:

  • START TRANSACTION;开始一个新事务。
  • COMMIT; 提交当前事务,将数据更改持久化到数据库。
  • ROLLBACK; 回滚当前事务,撤销所有未提交的更改。
  • SAVEPOINT 保存点名; 在事务中创建一个保存点。
  • ROLLBACK TO 保存点名; 回滚到保存点。

事务的隔离级别:

  • READ UNCOMMITTED 未提交读,最低隔离级别,可能读取"脏"数据。
  • READ COMMITTED 提交读,避免"脏"数据,但有重复读和幻读问题。
  • REPEATABLE READ 重复读,事务中多次读取相同,避免幻读。
  • SERIALIZABLE 串行化,代价最高,完全避免并发问题。

9.2JDBC中事务实现的逻辑

try{
    connection.setAutoCommit(false); //关闭自动提交了
    //connection.setAutoCommit(false)也就类型于SET autocommit = off
    
    //注意,只要当前connection对象,进行数据库操作,都不会自动提交事务
    //数据库动作!
    //prepareStatement - 单一的数据库动作 c r u d 
    //connection - 操作事务 
    
    //所有操作执行正确,提交事务!
    connection.commit();
  }catch(Execption e){
    //出现异常,则回滚事务!
    connection.rollback();
  }

9.3JDBC事务代码实现

环境搭建:

-- 继续在rainsoul的库中创建银行表
CREATE TABLE t_bank(
   id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
   account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
   money  INT UNSIGNED COMMENT '金额,不能为负值') ;
   
INSERT INTO t_bank(account,money) VALUES
  ('zhangsan',1000),('lisi',1000);

image.png

  1. Dao接口:
public interface BankDao{
    int addMoney(Integer id,Integer money);

    int subMoney(Integer id,Integer money);
}
  1. Dao接口实现类:
public class BankDaoImpl extends BaseDAO implements BankDao {  
    @Override  
    public int addMoney(Integer id, Integer money) {  
        try {  
            String sql = "UPDATE t_bank SET money = money + ? WHERE id = ?";  
            return executeUpdate(sql,money,id);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public int subMoney(Integer id, Integer money) {  
        try {  
            String sql = "UPDATE t_bank SET money = money - ? WHERE id = ?";  
            return executeUpdate(sql,money,id);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
}
  1. 测试代码:
    /**
     * 测试银行转账的事务功能。
     * 该方法模拟从一个账户(账号1)向另一个账户(账号2)转账100元的过程。
     * 如果整个过程中发生异常,将进行事务回滚,确保数据一致性。
     * 无参数和返回值。
     */
    @Test
    public void testTransaction(){
        BankDao bankDao = new BankDaoImpl();
        Connection connection=null;
        try {
            // 1. 获取数据库连接并开启事务
            connection = JDBCUtilV2.getConnection();
            connection.setAutoCommit(false); // 开启事务,将自动提交设置为false,以控制事务手动提交

            // 2. 执行扣款操作
            bankDao.subMoney(1,100);

            int i = 10 / 0; // 模拟运行时异常

            // 3. 执行加款操作
            bankDao.addMoney(2,100);

            // 4. 如果之前的操作没有异常,则提交事务
            connection.commit();
        } catch (Exception e) {
            // 发生异常时,回滚事务
            try {
                connection.rollback();
            } catch (Exception ex) {
                // 抛出运行时异常,以便上层调用者能够处理
                throw new RuntimeException(ex);
            }
        }finally {
            // 释放数据库资源
            JDBCUtilV2.release();
        }
    }

下一阶段:Javaweb。